From f2c145b2fc9d6ffce838398e06f7aa583d05887d Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Sat, 5 Jan 2019 16:34:10 -0600 Subject: Change qPrintable to qUtf8Printable to better support non-ASCII text --- src/nexusinterface.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index e97e9800..c6f05405 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -226,7 +226,7 @@ void NexusInterface::interpretNexusFileName(const QString &fileName, QString &mo } else { modID = strtol(candidate.c_str(), nullptr, 10); } - qDebug("mod id guessed: %s -> %d", qPrintable(fileName), modID); + qDebug("mod id guessed: %s -> %d", qUtf8Printable(fileName), modID); } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) { qDebug("simple expression matched, using name only"); modName = QString::fromUtf8(result[1].str().c_str()); @@ -542,7 +542,7 @@ void NexusInterface::requestFinished(std::list::iterator iter) if (nexusError.length() == 0) { nexusError = tr("empty response"); } - qDebug("nexus error: %s", qPrintable(nexusError)); + qDebug("nexus error: %s", qUtf8Printable(nexusError)); emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, nexusError); } else { bool ok; @@ -602,8 +602,8 @@ void NexusInterface::requestError(QNetworkReply::NetworkError) } qCritical("request (%s) error: %s (%d)", - qPrintable(reply->url().toString()), - qPrintable(reply->errorString()), + qUtf8Printable(reply->url().toString()), + qUtf8Printable(reply->errorString()), reply->error()); } -- cgit v1.3.1 From 0cac03cdf6cbfb7634a5f3875814b4c1aabe8714 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Wed, 30 Jan 2019 22:16:38 -0600 Subject: Always display at least 3 segments of version info for MO itself --- src/main.cpp | 2 +- src/mainwindow.cpp | 4 ++-- src/modlist.cpp | 2 +- src/nexusinterface.cpp | 2 +- src/selfupdater.cpp | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/main.cpp b/src/main.cpp index 190a8f4b..0f6d7048 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -446,7 +446,7 @@ static void preloadSsl() static QString getVersionDisplayString() { - return createVersionInfo().displayString(); + return createVersionInfo().displayString(3); } int runApplication(MOApplication &application, SingleInstance &instance, diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9904845b..d35d9951 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -502,7 +502,7 @@ void MainWindow::updateWindowTitle(const QString &accountName, bool premium) { QString title = QString("%1 Mod Organizer v%2").arg( m_OrganizerCore.managedGame()->gameName(), - m_OrganizerCore.getVersion().displayString()); + m_OrganizerCore.getVersion().displayString(3)); if (!accountName.isEmpty()) { title.append(QString(" (%1%2)").arg(accountName, premium ? "*" : "")); @@ -719,7 +719,7 @@ size_t MainWindow::checkForProblems() void MainWindow::about() { - AboutDialog dialog(m_OrganizerCore.getVersion().displayString(), this); + AboutDialog dialog(m_OrganizerCore.getVersion().displayString(3), this); connect(&dialog, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); dialog.exec(); } diff --git a/src/modlist.cpp b/src/modlist.cpp index 224818c8..62c186a4 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -449,7 +449,7 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const return QString(); } } else if (column == COL_VERSION) { - QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString()).arg(modInfo->getNewestVersion().displayString()); + QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString(3)).arg(modInfo->getNewestVersion().displayString(3)); if (modInfo->downgradeAvailable()) { text += "
" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn " "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. " diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index c6f05405..993ae41e 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -149,7 +149,7 @@ NexusInterface::NexusInterface(PluginContainer *pluginContainer) { m_MOVersion = createVersionInfo(); - m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString()); + m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); m_DiskCache = new QNetworkDiskCache(this); connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); } diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp index b5e7684c..37700e08 100644 --- a/src/selfupdater.cpp +++ b/src/selfupdater.cpp @@ -145,15 +145,15 @@ void SelfUpdater::testForUpdate() if (newestVer > this->m_MOVersion) { m_UpdateCandidate = newest; qDebug("update available: %s -> %s", - qUtf8Printable(this->m_MOVersion.displayString()), - qUtf8Printable(newestVer.displayString())); + qUtf8Printable(this->m_MOVersion.displayString(3)), + qUtf8Printable(newestVer.displayString(3))); emit updateAvailable(); } else if (newestVer < this->m_MOVersion) { // this could happen if the user switches from using prereleases to // stable builds. Should we downgrade? qDebug("This version is newer than the latest released one: %s -> %s", - qUtf8Printable(this->m_MOVersion.displayString()), - qUtf8Printable(newestVer.displayString())); + qUtf8Printable(this->m_MOVersion.displayString(3)), + qUtf8Printable(newestVer.displayString(3))); } } }); -- cgit v1.3.1 From 9fe2f6126dc7b9396d188b57ccb097f0035f57b7 Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 20 Aug 2018 18:34:31 -0500 Subject: Initial Nexus API changes: - Switch to SSO with WebSocket - Update endpoints (all but version checking) --- src/CMakeLists.txt | 7 +- src/credentialsdialog.cpp | 10 - src/credentialsdialog.h | 3 - src/downloadmanager.cpp | 19 +- src/main.cpp | 5 +- src/mainwindow.cpp | 13112 ++++++++++++++++++++++---------------------- src/modinforegular.cpp | 23 +- src/nexusinterface.cpp | 101 +- src/nexusinterface.h | 10 +- src/nxmaccessmanager.cpp | 615 +-- src/nxmaccessmanager.h | 61 +- src/organizer_en.ts | 2936 +++++----- src/organizercore.cpp | 4946 +++++++++-------- src/organizercore.h | 5 +- src/settings.cpp | 48 +- src/settings.h | 12 +- src/settingsdialog.cpp | 42 + src/settingsdialog.h | 340 +- src/settingsdialog.ui | 44 +- 19 files changed, 10893 insertions(+), 11446 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cbd5aa9..a518ccd4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -267,6 +267,7 @@ FIND_PACKAGE(Qt5Quick REQUIRED) FIND_PACKAGE(Qt5Network REQUIRED) FIND_PACKAGE(Qt5WinExtras REQUIRED) FIND_PACKAGE(Qt5WebEngineWidgets REQUIRED) +FIND_PACKAGE(Qt5WebSockets REQUIRED) FIND_PACKAGE(Qt5Qml REQUIRED) FIND_PACKAGE(Qt5LinguistTools) QT5_WRAP_UI(organizer_UIHDRS ${organizer_UIS}) @@ -325,7 +326,7 @@ ENDIF() ADD_EXECUTABLE(ModOrganizer WIN32 ${organizer_HDRS} ${organizer_SRCS} ${organizer_UIHDRS} ${organizer_RCS} ${organizer_RCCPPS} ${organizer_translations_qm}) TARGET_LINK_LIBRARIES(ModOrganizer Qt5::Widgets Qt5::WinExtras Qt5::WebEngineWidgets Qt5::Quick - Qt5::Qml Qt5::QuickWidgets Qt5::Network + Qt5::Qml Qt5::QuickWidgets Qt5::Network Qt5::WebSockets ${Boost_LIBRARIES} zlibstatic uibase esptk bsatk githubpp @@ -368,12 +369,12 @@ SET(windeploy_parameters "--no-translations --plugindir qtplugins --libdir dlls INSTALL( CODE "EXECUTE_PROCESS(COMMAND - ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets ${windeploy_parameters} + ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets --websockets ${windeploy_parameters} WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin ) # run it a second time because on the first run it misses some files EXECUTE_PROCESS(COMMAND - ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets ${windeploy_parameters} + ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets --websockets ${windeploy_parameters} WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin ) EXECUTE_PROCESS(COMMAND diff --git a/src/credentialsdialog.cpp b/src/credentialsdialog.cpp index 04774548..73e75387 100644 --- a/src/credentialsdialog.cpp +++ b/src/credentialsdialog.cpp @@ -32,16 +32,6 @@ CredentialsDialog::~CredentialsDialog() delete ui; } -QString CredentialsDialog::username() const -{ - return ui->usernameEdit->text(); -} - -QString CredentialsDialog::password() const -{ - return ui->passwordEdit->text(); -} - bool CredentialsDialog::store() const { return ui->rememberCheck->isChecked(); diff --git a/src/credentialsdialog.h b/src/credentialsdialog.h index 8a68c7d8..2f3bcbb7 100644 --- a/src/credentialsdialog.h +++ b/src/credentialsdialog.h @@ -34,9 +34,6 @@ public: explicit CredentialsDialog(QWidget *parent = 0); ~CredentialsDialog(); - QString username() const; - QString password() const; - bool store() const; bool neverAsk() const; diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index ab8da7b8..c927695e 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -1536,10 +1536,10 @@ void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int file if (!info->version.isValid()) { info->version = info->newestVersion; } - info->fileName = result["uri"].toString(); + info->fileName = result["file_name"].toString(); info->fileCategory = result["category_id"].toInt(); - info->fileTime = matchDate(result["date"].toString()); - info->description = BBCode::convertToHTML(result["description"].toString()); + info->fileTime = matchDate(result["uploaded_timestamp"].toString()); + info->description = BBCode::convertToHTML(result["changelog_html"].toString()); info->repository = "Nexus"; info->gameName = gameName; @@ -1554,23 +1554,12 @@ static int evaluateFileInfoMap(const QVariantMap &map, const std::mapsecond * 20; } - if (map["IsPremium"].toBool()) result += 5; - return result; } diff --git a/src/main.cpp b/src/main.cpp index 275845e2..631ec5b5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -621,7 +621,10 @@ int runApplication(MOApplication &application, SingleInstance &instance, splash.show(); splash.activateWindow(); - NexusInterface::instance(&pluginContainer)->getAccessManager()->startLoginCheck(); + QString apiKey; + if (organizer.settings().getNexusApiKey(apiKey)) { + NexusInterface::instance(&pluginContainer)->getAccessManager()->apiCheck(apiKey); + } qDebug("initializing tutorials"); TutorialManager::init( diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a30fdaac..add1ef7f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,6554 +1,6558 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "mainwindow.h" -#include "ui_mainwindow.h" - -#include "directoryentry.h" -#include "directoryrefresher.h" -#include "executableinfo.h" -#include "executableslist.h" -#include "guessedvalue.h" -#include "imodinterface.h" -#include "iplugingame.h" -#include "iplugindiagnose.h" -#include "isavegame.h" -#include "isavegameinfowidget.h" -#include "nexusinterface.h" -#include "organizercore.h" -#include "pluginlistsortproxy.h" -#include "previewgenerator.h" -#include "serverinfo.h" -#include "savegameinfo.h" -#include "spawn.h" -#include "versioninfo.h" -#include "instancemanager.h" - -#include "report.h" -#include "modlist.h" -#include "modlistsortproxy.h" -#include "qtgroupingproxy.h" -#include "profile.h" -#include "pluginlist.h" -#include "profilesdialog.h" -#include "editexecutablesdialog.h" -#include "categories.h" -#include "categoriesdialog.h" -#include "modinfodialog.h" -#include "overwriteinfodialog.h" -#include "activatemodsdialog.h" -#include "downloadlist.h" -#include "downloadlistwidget.h" -#include "messagedialog.h" -#include "installationmanager.h" -#include "lockeddialog.h" -#include "waitingonclosedialog.h" -#include "logbuffer.h" -#include "downloadlistsortproxy.h" -#include "motddialog.h" -#include "filedialogmemory.h" -#include "tutorialmanager.h" -#include "modflagicondelegate.h" -#include "genericicondelegate.h" -#include "selectiondialog.h" -#include "csvbuilder.h" -#include "savetextasdialog.h" -#include "problemsdialog.h" -#include "previewdialog.h" -#include "browserdialog.h" -#include "aboutdialog.h" -#include -#include "nxmaccessmanager.h" -#include "appconfig.h" -#include "eventfilter.h" -#include -#include -#include -#include -#include -#include -#include "localsavegames.h" -#include "listdialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#ifndef Q_MOC_RUN -#include -#include -#include -#include -#include -#endif - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef TEST_MODELS -#include "modeltest.h" -#endif // TEST_MODELS - -#pragma warning( disable : 4428 ) - -using namespace MOBase; -using namespace MOShared; - - -MainWindow::MainWindow(QSettings &initSettings - , OrganizerCore &organizerCore - , PluginContainer &pluginContainer - , QWidget *parent) - : QMainWindow(parent) - , ui(new Ui::MainWindow) - , m_WasVisible(false) - , m_Tutorial(this, "MainWindow") - , m_OldProfileIndex(-1) - , m_ModListGroupingProxy(nullptr) - , m_ModListSortProxy(nullptr) - , m_OldExecutableIndex(-1) - , m_CategoryFactory(CategoryFactory::instance()) - , m_ContextItem(nullptr) - , m_ContextAction(nullptr) - , m_ContextRow(-1) - , m_CurrentSaveView(nullptr) - , m_OrganizerCore(organizerCore) - , m_PluginContainer(pluginContainer) - , m_DidUpdateMasterList(false) - , m_ArchiveListWriter(std::bind(&MainWindow::saveArchiveList, this)) -{ - QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); - QWebEngineProfile::defaultProfile()->setHttpCacheMaximumSize(52428800); - QWebEngineProfile::defaultProfile()->setCachePath(m_OrganizerCore.settings().getCacheDirectory()); - QWebEngineProfile::defaultProfile()->setPersistentStoragePath(m_OrganizerCore.settings().getCacheDirectory()); - ui->setupUi(this); - updateWindowTitle(QString(), false); - - languageChange(m_OrganizerCore.settings().language()); - - m_CategoryFactory.loadCategories(); - - ui->logList->setModel(LogBuffer::instance()); - ui->logList->setColumnWidth(0, 100); - ui->logList->setAutoScroll(true); - ui->logList->scrollToBottom(); - ui->logList->addAction(ui->actionCopy_Log_to_Clipboard); - int splitterSize = this->size().height(); // actually total window size, but the splitter doesn't seem to return the true value - ui->topLevelSplitter->setSizes(QList() << splitterSize - 100 << 100); - connect(ui->logList->model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), - ui->logList, SLOT(scrollToBottom())); - connect(ui->logList->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), - ui->logList, SLOT(scrollToBottom())); - - m_RefreshProgress = new QProgressBar(statusBar()); - m_RefreshProgress->setTextVisible(true); - m_RefreshProgress->setRange(0, 100); - m_RefreshProgress->setValue(0); - m_RefreshProgress->setVisible(false); - statusBar()->addWidget(m_RefreshProgress, 1000); - statusBar()->clearMessage(); - statusBar()->hide(); - - updateProblemsButton(); - - // Setup toolbar - QWidget *spacer = new QWidget(ui->toolBar); - spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - QWidget *widget = ui->toolBar->widgetForAction(ui->actionTool); - QToolButton *toolBtn = qobject_cast(widget); - - if (toolBtn->menu() == nullptr) { - actionToToolButton(ui->actionTool); - } - - actionToToolButton(ui->actionHelp); - createHelpWidget(); - - actionToToolButton(ui->actionEndorseMO); - createEndorseWidget(); - ui->actionEndorseMO->setVisible(false); - - for (QAction *action : ui->toolBar->actions()) { - if (action->isSeparator()) { - // insert spacers - ui->toolBar->insertWidget(action, spacer); - m_Sep = action; - // m_Sep would only use the last separator anyway, and we only have the one anyway? - break; - } - } - - TaskProgressManager::instance().tryCreateTaskbar(); - - // set up mod list - m_ModListSortProxy = m_OrganizerCore.createModListProxyModel(); - - ui->modList->setModel(m_ModListSortProxy); - - GenericIconDelegate *contentDelegate = new GenericIconDelegate(ui->modList, Qt::UserRole + 3, ModList::COL_CONTENT, 150); - connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), contentDelegate, SLOT(columnResized(int,int,int))); - ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder); - ModFlagIconDelegate *flagDelegate = new ModFlagIconDelegate(ui->modList, ModList::COL_FLAGS, 120); - connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), flagDelegate, SLOT(columnResized(int,int,int))); - ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate); - ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate); - ui->modList->header()->installEventFilter(m_OrganizerCore.modList()); - connect(ui->modList->header(), SIGNAL(sectionResized(int, int, int)), this, SLOT(modListSectionResized(int, int, int))); - - bool modListAdjusted = registerWidgetState(ui->modList->objectName(), ui->modList->header(), "mod_list_state"); - - if (modListAdjusted) { - // hack: force the resize-signal to be triggered because restoreState doesn't seem to do that - for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) { - int sectionSize = ui->modList->header()->sectionSize(column); - ui->modList->header()->resizeSection(column, sectionSize + 1); - ui->modList->header()->resizeSection(column, sectionSize); - } - } else { - // hide these columns by default - ui->modList->header()->setSectionHidden(ModList::COL_CONTENT, true); - ui->modList->header()->setSectionHidden(ModList::COL_MODID, true); - ui->modList->header()->setSectionHidden(ModList::COL_GAME, true); - ui->modList->header()->setSectionHidden(ModList::COL_INSTALLTIME, true); - ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true); - } - - ui->modList->header()->setSectionHidden(ModList::COL_NAME, false); // prevent the name-column from being hidden - ui->modList->installEventFilter(m_OrganizerCore.modList()); - - // set up plugin list - m_PluginListSortProxy = m_OrganizerCore.createPluginListProxyModel(); - - ui->espList->setModel(m_PluginListSortProxy); - ui->espList->sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder); - ui->espList->setItemDelegateForColumn(PluginList::COL_FLAGS, new GenericIconDelegate(ui->espList)); - ui->espList->installEventFilter(m_OrganizerCore.pluginList()); - - ui->bsaList->setLocalMoveOnly(true); - - initDownloadView(); - bool pluginListAdjusted = registerWidgetState(ui->espList->objectName(), ui->espList->header(), "plugin_list_state"); - registerWidgetState(ui->dataTree->objectName(), ui->dataTree->header()); - registerWidgetState(ui->downloadView->objectName(), - ui->downloadView->header()); - - ui->splitter->setStretchFactor(0, 3); - ui->splitter->setStretchFactor(1, 2); - - resizeLists(modListAdjusted, pluginListAdjusted); - - QMenu *linkMenu = new QMenu(this); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Toolbar"), this, SLOT(linkToolbar())); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this, SLOT(linkDesktop())); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); - ui->linkButton->setMenu(linkMenu); - - QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn); - initModListContextMenu(listOptionsMenu); - ui->listOptionsBtn->setMenu(listOptionsMenu); - connect(ui->listOptionsBtn, SIGNAL(pressed()), this, SLOT(on_listOptionsBtn_pressed())); - - ui->openFolderMenu->setMenu(openFolderMenu()); - - ui->savegameList->installEventFilter(this); - ui->savegameList->setMouseTracking(true); - - // don't allow mouse wheel to switch grouping, too many people accidentally - // turn on grouping and then don't understand what happened - EventFilter *noWheel - = new EventFilter(this, [](QObject *, QEvent *event) -> bool { - return event->type() == QEvent::Wheel; - }); - - ui->groupCombo->installEventFilter(noWheel); - ui->profileBox->installEventFilter(noWheel); - - if (organizerCore.managedGame()->sortMechanism() == MOBase::IPluginGame::SortMechanism::NONE) { - ui->bossButton->setDisabled(true); - ui->bossButton->setToolTip(tr("There is no supported sort mechanism for this game. You will probably have to use a third-party tool.")); - } - - connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*))); - - connect(ui->modList, SIGNAL(dropModeUpdate(bool)), m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool))); - - connect(ui->modList->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(modlistSelectionChanged(QModelIndex,QModelIndex))); - connect(m_ModListSortProxy, SIGNAL(filterActive(bool)), this, SLOT(modFilterActive(bool))); - connect(m_ModListSortProxy, SIGNAL(layoutChanged()), this, SLOT(updateModCount())); - connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString))); - - connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), m_PluginListSortProxy, SLOT(updateFilter(QString))); - connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString))); - - connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*))); - - connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed())); - connect(m_OrganizerCore.directoryRefresher(), SIGNAL(progress(int)), this, SLOT(refresher_progress(int))); - connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString))); - - connect(&m_SavesWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(refreshSavesIfOpen())); - - connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString))); - connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString))); - - connect(m_OrganizerCore.updater(), SIGNAL(restart()), this, SLOT(close())); - connect(m_OrganizerCore.updater(), SIGNAL(updateAvailable()), this, SLOT(updateAvailable())); - connect(m_OrganizerCore.updater(), SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString))); - - connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); - connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int))); - connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusLogin())); - connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); - connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), - this, SLOT(updateWindowTitle(const QString&, bool))); - - connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); - connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int))); - connect(ui->modList->header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), this, SLOT(modListSortIndicatorChanged(int,Qt::SortOrder))); - connect(ui->toolBar, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(toolBar_customContextMenuRequested(QPoint))); - - connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled); - connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close); - - connect(&m_IntegratedBrowser, SIGNAL(requestDownload(QUrl,QNetworkReply*)), &m_OrganizerCore, SLOT(requestDownload(QUrl,QNetworkReply*))); - - connect(this, SIGNAL(styleChanged(QString)), this, SLOT(updateStyle(QString))); - - m_CheckBSATimer.setSingleShot(true); - connect(&m_CheckBSATimer, SIGNAL(timeout()), this, SLOT(checkBSAList())); - - connect(ui->espList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(esplistSelectionsChanged(QItemSelection))); - connect(ui->modList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(modlistSelectionsChanged(QItemSelection))); - - new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated())); - new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated())); - - new QShortcut(QKeySequence::Refresh, this, SLOT(refreshProfile_activated())); - - new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F), this, SLOT(search_activated())); - new QShortcut(QKeySequence(Qt::Key_Escape), this, SLOT(searchClear_activated())); - - m_UpdateProblemsTimer.setSingleShot(true); - connect(&m_UpdateProblemsTimer, SIGNAL(timeout()), this, SLOT(updateProblemsButton())); - - m_SaveMetaTimer.setSingleShot(false); - connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); - m_SaveMetaTimer.start(5000); - - setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); - FileDialogMemory::restore(initSettings); - - fixCategories(); - - m_StartTime = QTime::currentTime(); - - m_Tutorial.expose("modList", m_OrganizerCore.modList()); - m_Tutorial.expose("espList", m_OrganizerCore.pluginList()); - - m_OrganizerCore.setUserInterface(this, this); - for (const QString &fileName : m_PluginContainer.pluginFileNames()) { - installTranslator(QFileInfo(fileName).baseName()); - } - - registerPluginTools(m_PluginContainer.plugins()); - - for (IPluginModPage *modPagePlugin : m_PluginContainer.plugins()) { - registerModPage(modPagePlugin); - } - - // refresh profiles so the current profile can be activated - refreshProfiles(false); - - ui->profileBox->setCurrentText(m_OrganizerCore.currentProfile()->name()); - - if (m_OrganizerCore.getArchiveParsing()) - { - ui->showArchiveDataCheckBox->setCheckState(Qt::Checked); - ui->showArchiveDataCheckBox->setEnabled(true); - m_showArchiveData = true; - } - else - { - ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked); - ui->showArchiveDataCheckBox->setEnabled(false); - m_showArchiveData = false; - } - - refreshExecutablesList(); - updateToolBar(); - - for (QAction *action : ui->toolBar->actions()) { - // set the name of the widget to the name of the action to allow styling - QWidget *actionWidget = ui->toolBar->widgetForAction(action); - actionWidget->setObjectName(action->objectName()); - actionWidget->style()->unpolish(actionWidget); - actionWidget->style()->polish(actionWidget); - } - - updatePluginCount(); - updateModCount(); -} - - -MainWindow::~MainWindow() -{ - try { - cleanup(); - - m_PluginContainer.setUserInterface(nullptr, nullptr); - m_OrganizerCore.setUserInterface(nullptr, nullptr); - m_IntegratedBrowser.close(); - delete ui; - } catch (std::exception &e) { - QMessageBox::critical(nullptr, tr("Crash on exit"), - tr("MO crashed while exiting. Some settings may not be saved.\n\nError: %1").arg(e.what()), - QMessageBox::Ok); - } -} - - -void MainWindow::updateWindowTitle(const QString &accountName, bool premium) -{ - QString title = QString("%1 Mod Organizer v%2").arg( - m_OrganizerCore.managedGame()->gameName(), - m_OrganizerCore.getVersion().displayString(3)); - - if (!accountName.isEmpty()) { - title.append(QString(" (%1%2)").arg(accountName, premium ? "*" : "")); - } - - this->setWindowTitle(title); -} - - -void MainWindow::disconnectPlugins() -{ - if (ui->actionTool->menu() != nullptr) { - ui->actionTool->menu()->clear(); - } -} - - -void MainWindow::resizeLists(bool modListCustom, bool pluginListCustom) -{ - if (!modListCustom) { - // resize mod list to fit content - for (int i = 0; i < ui->modList->header()->count(); ++i) { - ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); - } - ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch); - } - - // ensure the columns aren't so small you can't see them any more - for (int i = 0; i < ui->modList->header()->count(); ++i) { - if (ui->modList->header()->sectionSize(i) < 10) { - ui->modList->header()->resizeSection(i, 10); - } - } - - if (!pluginListCustom) { - // resize plugin list to fit content - for (int i = 0; i < ui->espList->header()->count(); ++i) { - ui->espList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); - } - ui->espList->header()->setSectionResizeMode(0, QHeaderView::Stretch); - } -} - - -void MainWindow::allowListResize() -{ - // allow resize on mod list - for (int i = 0; i < ui->modList->header()->count(); ++i) { - ui->modList->header()->setSectionResizeMode(i, QHeaderView::Interactive); - } - //ui->modList->header()->setSectionResizeMode(ui->modList->header()->count() - 1, QHeaderView::Stretch); - ui->modList->header()->setStretchLastSection(true); - - - // allow resize on plugin list - for (int i = 0; i < ui->espList->header()->count(); ++i) { - ui->espList->header()->setSectionResizeMode(i, QHeaderView::Interactive); - } - //ui->espList->header()->setSectionResizeMode(ui->espList->header()->count() - 1, QHeaderView::Stretch); - ui->espList->header()->setStretchLastSection(true); -} - -void MainWindow::updateStyle(const QString&) -{ - // no effect? - ensurePolished(); -} - -void MainWindow::resizeEvent(QResizeEvent *event) -{ - m_Tutorial.resize(event->size()); - QMainWindow::resizeEvent(event); -} - - -static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex idx) -{ - QModelIndex result = idx; - const QAbstractItemModel *model = idx.model(); - while (model != targetModel) { - if (model == nullptr) { - return QModelIndex(); - } - const QAbstractProxyModel *proxyModel = qobject_cast(model); - if (proxyModel == nullptr) { - return QModelIndex(); - } - result = proxyModel->mapToSource(result); - model = proxyModel->sourceModel(); - } - return result; -} - - -void MainWindow::actionToToolButton(QAction *&sourceAction) -{ - QToolButton *button = new QToolButton(ui->toolBar); - button->setObjectName(sourceAction->objectName()); - button->setIcon(sourceAction->icon()); - button->setText(sourceAction->text()); - button->setPopupMode(QToolButton::InstantPopup); - button->setToolButtonStyle(ui->toolBar->toolButtonStyle()); - button->setToolTip(sourceAction->toolTip()); - button->setShortcut(sourceAction->shortcut()); - QMenu *buttonMenu = new QMenu(sourceAction->text(), button); - button->setMenu(buttonMenu); - QAction *newAction = ui->toolBar->insertWidget(sourceAction, button); - newAction->setObjectName(sourceAction->objectName()); - newAction->setIcon(sourceAction->icon()); - newAction->setText(sourceAction->text()); - newAction->setToolTip(sourceAction->toolTip()); - newAction->setShortcut(sourceAction->shortcut()); - ui->toolBar->removeAction(sourceAction); - sourceAction->deleteLater(); - sourceAction = newAction; -} - -void MainWindow::updateToolBar() -{ - for (QAction *action : ui->toolBar->actions()) { - if (action->objectName().startsWith("custom__")) { - ui->toolBar->removeAction(action); - action->deleteLater(); - } - } - - std::vector::iterator begin, end; - m_OrganizerCore.executablesList()->getExecutables(begin, end); - for (auto iter = begin; iter != end; ++iter) { - if (iter->isShownOnToolbar()) { - QAction *exeAction = new QAction(iconForExecutable(iter->m_BinaryInfo.filePath()), - iter->m_Title, - ui->toolBar); - exeAction->setObjectName(QString("custom__") + iter->m_Title); - if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) { - qDebug("failed to connect trigger?"); - } - ui->toolBar->insertAction(m_Sep, exeAction); - } - } -} - - -void MainWindow::scheduleUpdateButton() -{ - if (!m_UpdateProblemsTimer.isActive()) { - m_UpdateProblemsTimer.start(1000); - } -} - - -void MainWindow::updateProblemsButton() -{ - size_t numProblems = checkForProblems(); - if (numProblems > 0) { - ui->actionNotifications->setEnabled(true); - ui->actionNotifications->setIconText(tr("Notifications")); - ui->actionNotifications->setToolTip(tr("There are notifications to read")); - - QPixmap mergedIcon = QPixmap(":/MO/gui/warning").scaled(64, 64); - { - QPainter painter(&mergedIcon); - 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())); - } - ui->actionNotifications->setIcon(QIcon(mergedIcon)); - } else { - ui->actionNotifications->setEnabled(false); - ui->actionNotifications->setIconText(tr("No Notifications")); - ui->actionNotifications->setToolTip(tr("There are no notifications")); - ui->actionNotifications->setIcon(QIcon(":/MO/gui/warning")); - } -} - - -bool MainWindow::errorReported(QString &logFile) -{ - QDir dir(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath())); - QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"), - QDir::Files, QDir::Name | QDir::Reversed); - - if (files.count() > 0) { - logFile = files.at(0).absoluteFilePath(); - QFile file(logFile); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - char buffer[1024]; - int line = 0; - while (!file.atEnd()) { - file.readLine(buffer, 1024); - if (strncmp(buffer, "ERROR", 5) == 0) { - return true; - } - - // prevent this function from taking forever - if (line++ >= 50000) { - break; - } - } - } - } - - return false; -} - - -size_t MainWindow::checkForProblems() -{ - size_t numProblems = 0; - for (IPluginDiagnose *diagnose : m_PluginContainer.plugins()) { - numProblems += diagnose->activeProblems().size(); - } - return numProblems; -} - -void MainWindow::about() -{ - AboutDialog dialog(m_OrganizerCore.getVersion().displayString(3), this); - connect(&dialog, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); - dialog.exec(); -} - - -void MainWindow::createEndorseWidget() -{ - QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionEndorseMO)); - QMenu *buttonMenu = toolBtn->menu(); - if (buttonMenu == nullptr) { - return; - } - buttonMenu->clear(); - - QAction *endorseAction = new QAction(tr("Endorse"), buttonMenu); - connect(endorseAction, SIGNAL(triggered()), this, SLOT(on_actionEndorseMO_triggered())); - buttonMenu->addAction(endorseAction); - - QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu); - connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(wontEndorse())); - buttonMenu->addAction(wontEndorseAction); -} - - -void MainWindow::createHelpWidget() -{ - QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionHelp)); - QMenu *buttonMenu = toolBtn->menu(); - if (buttonMenu == nullptr) { - return; - } - buttonMenu->clear(); - - QAction *helpAction = new QAction(tr("Help on UI"), buttonMenu); - connect(helpAction, SIGNAL(triggered()), this, SLOT(helpTriggered())); - buttonMenu->addAction(helpAction); - - QAction *wikiAction = new QAction(tr("Documentation"), buttonMenu); - connect(wikiAction, SIGNAL(triggered()), this, SLOT(wikiTriggered())); - buttonMenu->addAction(wikiAction); - - QAction *discordAction = new QAction(tr("Chat on Discord"), buttonMenu); - connect(discordAction, SIGNAL(triggered()), this, SLOT(discordTriggered())); - buttonMenu->addAction(discordAction); - - QAction *issueAction = new QAction(tr("Report Issue"), buttonMenu); - connect(issueAction, SIGNAL(triggered()), this, SLOT(issueTriggered())); - buttonMenu->addAction(issueAction); - - QMenu *tutorialMenu = new QMenu(tr("Tutorials"), buttonMenu); - - typedef std::vector > ActionList; - - ActionList tutorials; - - QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); - while (dirIter.hasNext()) { - dirIter.next(); - QString fileName = dirIter.fileName(); - - QFile file(dirIter.filePath()); - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open " << fileName; - continue; - } - QString firstLine = QString::fromUtf8(file.readLine()); - if (firstLine.startsWith("//TL")) { - QStringList params = firstLine.mid(4).trimmed().split('#'); - if (params.size() != 2) { - qCritical() << "invalid header line for tutorial " << fileName << " expected 2 parameters"; - continue; - } - QAction *tutAction = new QAction(params.at(0), tutorialMenu); - tutAction->setData(fileName); - tutorials.push_back(std::make_pair(params.at(1).toInt(), tutAction)); - } - } - - std::sort(tutorials.begin(), tutorials.end(), - [] (const ActionList::value_type &LHS, const ActionList::value_type &RHS) { - return LHS.first < RHS.first; } ); - - for (auto iter = tutorials.begin(); iter != tutorials.end(); ++iter) { - connect(iter->second, SIGNAL(triggered()), this, SLOT(tutorialTriggered())); - tutorialMenu->addAction(iter->second); - } - - buttonMenu->addMenu(tutorialMenu); - buttonMenu->addAction(tr("About"), this, SLOT(about())); - buttonMenu->addAction(tr("About Qt"), qApp, SLOT(aboutQt())); -} - -void MainWindow::modFilterActive(bool filterActive) -{ - ui->clearFiltersButton->setVisible(filterActive); - if (filterActive) { -// m_OrganizerCore.modList()->setOverwriteMarkers(std::set(), std::set()); - ui->modList->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); - ui->activeModsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }"); - } else if (ui->groupCombo->currentIndex() != 0) { - ui->modList->setStyleSheet("QTreeView { border: 2px ridge #337733; }"); - ui->activeModsCounter->setStyleSheet(""); - } else { - ui->modList->setStyleSheet(""); - ui->activeModsCounter->setStyleSheet(""); - } -} - -void MainWindow::espFilterChanged(const QString &filter) -{ - if (!filter.isEmpty()) { - ui->espList->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); - ui->activePluginsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }"); - } else { - ui->espList->setStyleSheet(""); - ui->activePluginsCounter->setStyleSheet(""); - } - updatePluginCount(); -} - -void MainWindow::downloadFilterChanged(const QString &filter) -{ - if (!filter.isEmpty()) { - ui->downloadView->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); - } else { - ui->downloadView->setStyleSheet(""); - } -} - -void MainWindow::expandModList(const QModelIndex &index) -{ - QAbstractItemModel *model = ui->modList->model(); -#pragma message("why is this so complicated? mapping the index doesn't work, probably a bug in QtGroupingProxy?") - for (int i = 0; i < model->rowCount(); ++i) { - QModelIndex targetIdx = model->index(i, 0); - if (model->data(targetIdx).toString() == index.data().toString()) { - ui->modList->expand(targetIdx); - break; - } - } -} - - -bool MainWindow::addProfile() -{ - QComboBox *profileBox = findChild("profileBox"); - bool okClicked = false; - - QString name = QInputDialog::getText(this, tr("Name"), - tr("Please enter a name for the new profile"), - QLineEdit::Normal, QString(), &okClicked); - if (okClicked && (name.size() > 0)) { - try { - profileBox->addItem(name); - profileBox->setCurrentIndex(profileBox->count() - 1); - return true; - } catch (const std::exception& e) { - reportError(tr("failed to create profile: %1").arg(e.what())); - return false; - } - } else { - return false; - } -} - -void MainWindow::hookUpWindowTutorials() -{ - QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); - while (dirIter.hasNext()) { - dirIter.next(); - QString fileName = dirIter.fileName(); - QFile file(dirIter.filePath()); - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open " << fileName; - continue; - } - QString firstLine = QString::fromUtf8(file.readLine()); - if (firstLine.startsWith("//WIN")) { - QString windowName = firstLine.mid(6).trimmed(); - if (!m_OrganizerCore.settings().directInterface().value("CompletedWindowTutorials/" + windowName, false).toBool()) { - TutorialManager::instance().activateTutorial(windowName, fileName); - } - } - } -} - -void MainWindow::showEvent(QShowEvent *event) -{ - refreshFilters(); - - QMainWindow::showEvent(event); - - if (!m_WasVisible) { - // only the first time the window becomes visible - m_Tutorial.registerControl(); - - hookUpWindowTutorials(); - - if (m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) { - QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial()); - if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) { - if (QMessageBox::question(this, tr("Show tutorial?"), - tr("You are starting Mod Organizer for the first time. " - "Do you want to show a tutorial of its basic features? If you choose " - "no you can always start the tutorial from the \"Help\"-menu."), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial); - } - } else { - qCritical() << firstStepsTutorial << " missing"; - QPoint pos = ui->toolBar->mapToGlobal(QPoint()); - pos.rx() += ui->toolBar->width() / 2; - pos.ry() += ui->toolBar->height(); - QWhatsThis::showText(pos, - QObject::tr("Please use \"Help\" from the toolbar to get usage instructions to all elements")); - } - - m_OrganizerCore.settings().directInterface().setValue("first_start", false); - } - - // this has no visible impact when called before the ui is visible - int grouping = m_OrganizerCore.settings().directInterface().value("group_state").toInt(); - ui->groupCombo->setCurrentIndex(grouping); - - allowListResize(); - - m_OrganizerCore.settings().registerAsNXMHandler(false); - m_WasVisible = true; - updateProblemsButton(); - } -} - - -void MainWindow::closeEvent(QCloseEvent* event) -{ - m_closing = true; - - if (m_OrganizerCore.downloadManager()->downloadsInProgressNoPause()) { - if (QMessageBox::question(this, tr("Downloads in progress"), - tr("There are still downloads in progress, do you really want to quit?"), - QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) { - event->ignore(); - return; - } else { - m_OrganizerCore.downloadManager()->pauseAll(); - } - } - - std::vector hiddenList; - hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); - HANDLE injected_process_still_running = m_OrganizerCore.findAndOpenAUSVFSProcess(hiddenList, GetCurrentProcessId()); - if (injected_process_still_running != INVALID_HANDLE_VALUE) - { - m_OrganizerCore.waitForApplication(injected_process_still_running); - if (!m_closing) { // if operation cancelled - event->ignore(); - return; - } - } - - setCursor(Qt::WaitCursor); -} - -void MainWindow::cleanup() -{ - if (ui->logList->model() != nullptr) { - disconnect(ui->logList->model(), nullptr, nullptr, nullptr); - ui->logList->setModel(nullptr); - } - - QWebEngineProfile::defaultProfile()->clearAllVisitedLinks(); - m_IntegratedBrowser.close(); - m_SaveMetaTimer.stop(); - m_MetaSave.waitForFinished(); -} - - -void MainWindow::setBrowserGeometry(const QByteArray &geometry) -{ - m_IntegratedBrowser.restoreGeometry(geometry); -} - -void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem) -{ - QString const &save = newItem->data(Qt::UserRole).toString(); - if (m_CurrentSaveView == nullptr) { - IPluginGame const *game = m_OrganizerCore.managedGame(); - SaveGameInfo const *info = game->feature(); - if (info != nullptr) { - m_CurrentSaveView = info->getSaveGameWidget(this); - } - if (m_CurrentSaveView == nullptr) { - return; - } - } - m_CurrentSaveView->setSave(save); - - QRect screenRect = QApplication::desktop()->availableGeometry(m_CurrentSaveView); - - QPoint pos = QCursor::pos(); - if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) { - pos.rx() -= (m_CurrentSaveView->width() + 2); - } else { - pos.rx() += 5; - } - - if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) { - pos.ry() -= (m_CurrentSaveView->height() + 10); - } else { - pos.ry() += 20; - } - m_CurrentSaveView->move(pos); - - m_CurrentSaveView->show(); - m_CurrentSaveView->setProperty("displayItem", qVariantFromValue(static_cast(newItem))); - - ui->savegameList->activateWindow(); -} - - -void MainWindow::saveSelectionChanged(QListWidgetItem *newItem) -{ - if (newItem == nullptr) { - hideSaveGameInfo(); - } else if (m_CurrentSaveView == nullptr || newItem != m_CurrentSaveView->property("displayItem").value()) { - displaySaveGameInfo(newItem); - } -} - - -void MainWindow::hideSaveGameInfo() -{ - if (m_CurrentSaveView != nullptr) { - m_CurrentSaveView->deleteLater(); - m_CurrentSaveView = nullptr; - } -} - -bool MainWindow::eventFilter(QObject *object, QEvent *event) -{ - if ((object == ui->savegameList) && - ((event->type() == QEvent::Leave) || (event->type() == QEvent::WindowDeactivate))) { - hideSaveGameInfo(); - } - return false; -} - - -void MainWindow::toolPluginInvoke() -{ - QAction *triggeredAction = qobject_cast(sender()); - IPluginTool *plugin = qobject_cast(triggeredAction->data().value()); - if (plugin != nullptr) { - try { - plugin->display(); - } catch (const std::exception &e) { - reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what())); - } catch (...) { - reportError(tr("Plugin \"%1\" failed").arg(plugin->name())); - } - } -} - -void MainWindow::modPagePluginInvoke() -{ - QAction *triggeredAction = qobject_cast(sender()); - IPluginModPage *plugin = qobject_cast(triggeredAction->data().value()); - if (plugin != nullptr) { - if (plugin->useIntegratedBrowser()) { - m_IntegratedBrowser.setWindowTitle(plugin->displayName()); - m_IntegratedBrowser.openUrl(plugin->pageURL()); - } else { - QDesktopServices::openUrl(QUrl(plugin->pageURL())); - } - } -} - -void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu) -{ - if (name.isEmpty()) - name = tool->displayName(); - - QAction *action = new QAction(tool->icon(), name, ui->toolBar); - action->setToolTip(tool->tooltip()); - tool->setParentWidget(this); - action->setData(qVariantFromValue((QObject*)tool)); - connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke()), Qt::QueuedConnection); - - if (menu == nullptr) { - QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionTool)); - toolBtn->menu()->addAction(action); - } else { - menu->addAction(action); - } -} - -void MainWindow::registerPluginTools(std::vector toolPlugins) -{ - // Sort the plugins by display name - std::sort(toolPlugins.begin(), toolPlugins.end(), - [](IPluginTool *left, IPluginTool *right) { - return left->displayName().toLower() < right->displayName().toLower(); - } - ); - - // Group the plugins into submenus - QMap>> submenuMap; - for (auto toolPlugin : toolPlugins) { - QStringList toolName = toolPlugin->displayName().split("/"); - QString submenu = toolName[0]; - toolName.pop_front(); - submenuMap[submenu].append(QPair(toolName.join("/"), toolPlugin)); - } - - // Start registering plugins - for (auto submenuKey : submenuMap.keys()) { - if (submenuMap[submenuKey].length() > 1) { - QMenu *submenu = new QMenu(submenuKey, this); - for (auto info : submenuMap[submenuKey]) { - registerPluginTool(info.second, info.first, submenu); - } - QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionTool)); - toolBtn->menu()->addMenu(submenu); - } - else { - registerPluginTool(submenuMap[submenuKey].front().second); - } - } -} - -void MainWindow::registerModPage(IPluginModPage *modPage) -{ - // turn the browser action into a drop-down menu if necessary - if (ui->actionNexus->menu() == nullptr) { - QAction *nexusAction = ui->actionNexus; - // TODO: use a different icon for nexus! - ui->actionNexus = new QAction(nexusAction->icon(), tr("Browse Mod Page"), ui->toolBar); - ui->toolBar->insertAction(nexusAction, ui->actionNexus); - ui->toolBar->removeAction(nexusAction); - actionToToolButton(ui->actionNexus); - - QToolButton *browserBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionNexus)); - browserBtn->menu()->addAction(nexusAction); - } - - QAction *action = new QAction(modPage->icon(), modPage->displayName(), ui->toolBar); - modPage->setParentWidget(this); - action->setData(qVariantFromValue(reinterpret_cast(modPage))); - - connect(action, SIGNAL(triggered()), this, SLOT(modPagePluginInvoke()), Qt::QueuedConnection); - QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionNexus)); - toolBtn->menu()->addAction(action); -} - - -void MainWindow::startExeAction() -{ - QAction *action = qobject_cast(sender()); - if (action != nullptr) { - const Executable &selectedExecutable(m_OrganizerCore.executablesList()->find(action->text())); - QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.m_Title).toString(); - auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.m_Title); - if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.m_Title)) { - forcedLibraries.clear(); - } - m_OrganizerCore.spawnBinary( - selectedExecutable.m_BinaryInfo, selectedExecutable.m_Arguments, - selectedExecutable.m_WorkingDirectory.length() != 0 - ? selectedExecutable.m_WorkingDirectory - : selectedExecutable.m_BinaryInfo.absolutePath(), - selectedExecutable.m_SteamAppID, - customOverwrite, - forcedLibraries); - } else { - qCritical("not an action?"); - } -} - - -void MainWindow::setExecutableIndex(int index) -{ - QComboBox *executableBox = findChild("executablesListBox"); - - if ((index != 0) && (executableBox->count() > index)) { - executableBox->setCurrentIndex(index); - } else { - executableBox->setCurrentIndex(1); - } -} - -void MainWindow::activateSelectedProfile() -{ - m_OrganizerCore.setCurrentProfile(ui->profileBox->currentText()); - - m_ModListSortProxy->setProfile(m_OrganizerCore.currentProfile()); - - refreshSaveList(); - m_OrganizerCore.refreshModList(); - updateModCount(); - updatePluginCount(); -} - -void MainWindow::on_profileBox_currentIndexChanged(int index) -{ - if (ui->profileBox->isEnabled()) { - int previousIndex = m_OldProfileIndex; - m_OldProfileIndex = index; - - if ((previousIndex != -1) && - (m_OrganizerCore.currentProfile() != nullptr) && - m_OrganizerCore.currentProfile()->exists()) { - m_OrganizerCore.saveCurrentLists(); - } - - // ensure the new index is valid - if (index < 0 || index >= ui->profileBox->count()) { - qDebug("invalid profile index, using last profile"); - ui->profileBox->setCurrentIndex(ui->profileBox->count() - 1); - } - - if (ui->profileBox->currentIndex() == 0) { - ui->profileBox->setCurrentIndex(previousIndex); - ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec(); - while (!refreshProfiles()) { - ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec(); - } - } else { - activateSelectedProfile(); - } - - LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature(); - if (saveGames != nullptr) { - if (saveGames->prepareProfile(m_OrganizerCore.currentProfile())) - refreshSaveList(); - } - - BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature(); - if (invalidation != nullptr) { - if (invalidation->prepareProfile(m_OrganizerCore.currentProfile())) - QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh())); - } - } -} - -void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon) -{ - bool isDirectory = true; - //QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); - //QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); - - std::wostringstream temp; - temp << directorySoFar << "\\" << directoryEntry.getName(); - { - std::vector::const_iterator current, end; - directoryEntry.getSubDirectories(current, end); - for (; current != end; ++current) { - QString pathName = ToQString((*current)->getName()); - QStringList columns(pathName); - columns.append(""); - if (!(*current)->isEmpty()) { - QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns); - directoryChild->setData(0, Qt::DecorationRole, *folderIcon); - directoryChild->setData(0, Qt::UserRole + 3, isDirectory); - - if (conflictsOnly || !m_showArchiveData) { - updateTo(directoryChild, temp.str(), **current, conflictsOnly, fileIcon, folderIcon); - if (directoryChild->childCount() != 0) { - subTree->addChild(directoryChild); - } - else { - delete directoryChild; - } - } - else { - QTreeWidgetItem *onDemandLoad = new QTreeWidgetItem(QStringList()); - onDemandLoad->setData(0, Qt::UserRole + 0, "__loaded_on_demand__"); - onDemandLoad->setData(0, Qt::UserRole + 1, ToQString(temp.str())); - onDemandLoad->setData(0, Qt::UserRole + 2, conflictsOnly); - directoryChild->addChild(onDemandLoad); - subTree->addChild(directoryChild); - } - } - else { - QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns); - directoryChild->setData(0, Qt::DecorationRole, *folderIcon); - directoryChild->setData(0, Qt::UserRole + 3, isDirectory); - subTree->addChild(directoryChild); - } - } - } - - - isDirectory = false; - { - for (const FileEntry::Ptr current : directoryEntry.getFiles()) { - if (conflictsOnly && (current->getAlternatives().size() == 0)) { - continue; - } - - bool isArchive = false; - int originID = current->getOrigin(isArchive); - if (!m_showArchiveData && isArchive) { - continue; - } - - QString fileName = ToQString(current->getName()); - QStringList columns(fileName); - FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID); - QString source("data"); - unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName())); - if (modIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - source = modInfo->name(); - } - - std::pair archive = current->getArchive(); - if (archive.first.length() != 0) { - source.append(" (").append(ToQString(archive.first)).append(")"); - } - columns.append(source); - QTreeWidgetItem *fileChild = new QTreeWidgetItem(columns); - if (isArchive) { - QFont font = fileChild->font(0); - font.setItalic(true); - fileChild->setFont(0, font); - fileChild->setFont(1, font); - } else if (fileName.endsWith(ModInfo::s_HiddenExt)) { - QFont font = fileChild->font(0); - font.setStrikeOut(true); - fileChild->setFont(0, font); - fileChild->setFont(1, font); - } - fileChild->setData(0, Qt::UserRole, ToQString(current->getFullPath())); - fileChild->setData(0, Qt::DecorationRole, *fileIcon); - fileChild->setData(0, Qt::UserRole + 3, isDirectory); - fileChild->setData(0, Qt::UserRole + 1, isArchive); - fileChild->setData(1, Qt::UserRole, source); - fileChild->setData(1, Qt::UserRole + 1, originID); - - std::vector>> alternatives = current->getAlternatives(); - - if (!alternatives.empty()) { - std::wostringstream altString; - altString << ToWString(tr("Also in:
")); - for (std::vector>>::iterator altIter = alternatives.begin(); - altIter != alternatives.end(); ++altIter) { - if (altIter != alternatives.begin()) { - altString << " , "; - } - altString << "" << m_OrganizerCore.directoryStructure()->getOriginByID(altIter->first).getName() << ""; - } - fileChild->setToolTip(1, QString("%1").arg(ToQString(altString.str()))); - fileChild->setForeground(1, QBrush(Qt::red)); - } else { - fileChild->setToolTip(1, tr("No conflict")); - } - subTree->addChild(fileChild); - } - } - - - //subTree->sortChildren(0, Qt::AscendingOrder); -} - -void MainWindow::delayedRemove() -{ - for (QTreeWidgetItem *item : m_RemoveWidget) { - item->removeChild(item->child(0)); - } - m_RemoveWidget.clear(); -} - -void MainWindow::expandDataTreeItem(QTreeWidgetItem *item) -{ - if ((item->childCount() == 1) && (item->child(0)->data(0, Qt::UserRole).toString() == "__loaded_on_demand__")) { - // read the data we need from the sub-item, then dispose of it - QTreeWidgetItem *onDemandDataItem = item->child(0); - std::wstring path = ToWString(onDemandDataItem->data(0, Qt::UserRole + 1).toString()); - bool conflictsOnly = onDemandDataItem->data(0, Qt::UserRole + 2).toBool(); - - std::wstring virtualPath = (path + L"\\").substr(6) + ToWString(item->text(0)); - DirectoryEntry *dir = m_OrganizerCore.directoryStructure()->findSubDirectoryRecursive(virtualPath); - if (dir != nullptr) { - QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); - QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); - updateTo(item, path, *dir, conflictsOnly, &fileIcon, &folderIcon); - } else { - qWarning("failed to update view of %ls", path.c_str()); - } - m_RemoveWidget.push_back(item); - QTimer::singleShot(5, this, SLOT(delayedRemove())); - } -} - - -bool MainWindow::refreshProfiles(bool selectProfile) -{ - QComboBox* profileBox = findChild("profileBox"); - - QString currentProfileName = profileBox->currentText(); - - profileBox->blockSignals(true); - profileBox->clear(); - profileBox->addItem(QObject::tr("")); - - QDir profilesDir(Settings::instance().getProfileDirectory()); - profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); - - QDirIterator profileIter(profilesDir); - - while (profileIter.hasNext()) { - profileIter.next(); - try { - profileBox->addItem(profileIter.fileName()); - } catch (const std::runtime_error& error) { - reportError(QObject::tr("failed to parse profile %1: %2").arg(profileIter.fileName()).arg(error.what())); - } - } - - // now select one of the profiles, preferably the one that was selected before - profileBox->blockSignals(false); - - if (selectProfile) { - if (profileBox->count() > 1) { - profileBox->setCurrentText(currentProfileName); - if (profileBox->currentIndex() == 0) { - profileBox->setCurrentIndex(1); - } - } - } - return profileBox->count() > 1; -} - - -void MainWindow::refreshExecutablesList() -{ - QComboBox* executablesList = findChild("executablesListBox"); - executablesList->setEnabled(false); - executablesList->clear(); - executablesList->addItem(tr("")); - - QAbstractItemModel *model = executablesList->model(); - - std::vector::const_iterator current, end; - m_OrganizerCore.executablesList()->getExecutables(current, end); - for(int i = 0; current != end; ++current, ++i) { - QIcon icon = iconForExecutable(current->m_BinaryInfo.filePath()); - executablesList->addItem(icon, current->m_Title); - model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole); - } - - setExecutableIndex(1); - executablesList->setEnabled(true); -} - - -void MainWindow::refreshDataTree() -{ - QCheckBox *conflictsBox = findChild("conflictsCheckBox"); - QTreeWidget *tree = findChild("dataTree"); - QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); - QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); - tree->clear(); - QStringList columns("data"); - columns.append(""); - QTreeWidgetItem *subTree = new QTreeWidgetItem(columns); - subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder)); - updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon); - tree->insertTopLevelItem(0, subTree); - subTree->setExpanded(true); -} - -void MainWindow::refreshDataTreeKeepExpandedNodes() -{ - QCheckBox *conflictsBox = findChild("conflictsCheckBox"); - QTreeWidget *tree = findChild("dataTree"); - QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); - QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); - QStringList expandedNodes; - QTreeWidgetItemIterator it1(tree, QTreeWidgetItemIterator::NotHidden | QTreeWidgetItemIterator::HasChildren); - while (*it1) { - QTreeWidgetItem *current = (*it1); - if (current->isExpanded() && !(current->text(0)=="data")) { - expandedNodes.append(current->text(0)+"/"+current->parent()->text(0)); - } - ++it1; - } - - tree->clear(); - QStringList columns("data"); - columns.append(""); - QTreeWidgetItem *subTree = new QTreeWidgetItem(columns); - subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder)); - updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon); - tree->insertTopLevelItem(0, subTree); - subTree->setExpanded(true); - QTreeWidgetItemIterator it2(tree, QTreeWidgetItemIterator::HasChildren); - while (*it2) { - QTreeWidgetItem *current = (*it2); - if (!(current->text(0)=="data") && expandedNodes.contains(current->text(0)+"/"+current->parent()->text(0))) { - current->setExpanded(true); - } - ++it2; - } -} - - -void MainWindow::refreshSavesIfOpen() -{ - if (ui->tabWidget->currentIndex() == 3) { - refreshSaveList(); - } -} - -QDir MainWindow::currentSavesDir() const -{ - QDir savesDir; - if (m_OrganizerCore.currentProfile()->localSavesEnabled()) { - savesDir.setPath(m_OrganizerCore.currentProfile()->savePath()); - } else { - QString iniPath = m_OrganizerCore.currentProfile()->localSettingsEnabled() - ? m_OrganizerCore.currentProfile()->absolutePath() - : m_OrganizerCore.managedGame()->documentsDirectory().absolutePath(); - iniPath += "/" + m_OrganizerCore.managedGame()->iniFiles()[0]; - - wchar_t path[MAX_PATH]; - ::GetPrivateProfileStringW( - L"General", L"SLocalSavePath", L"Saves", - path, MAX_PATH, - iniPath.toStdWString().c_str() - ); - savesDir.setPath(m_OrganizerCore.managedGame()->documentsDirectory().absoluteFilePath(QString::fromWCharArray(path))); - } - - return savesDir; -} - -void MainWindow::startMonitorSaves() -{ - stopMonitorSaves(); - - QDir savesDir = currentSavesDir(); - - m_SavesWatcher.addPath(savesDir.absolutePath()); -} - -void MainWindow::stopMonitorSaves() -{ - if (m_SavesWatcher.directories().length() > 0) { - m_SavesWatcher.removePaths(m_SavesWatcher.directories()); - } -} - -void MainWindow::refreshSaveList() -{ - ui->savegameList->clear(); - - startMonitorSaves(); // re-starts monitoring - - QStringList filters; - filters << QString("*.") + m_OrganizerCore.managedGame()->savegameExtension(); - - QDir savesDir = currentSavesDir(); - savesDir.setNameFilters(filters); - qDebug("reading save games from %s", qUtf8Printable(savesDir.absolutePath())); - - QFileInfoList files = savesDir.entryInfoList(QDir::Files, QDir::Time); - for (const QFileInfo &file : files) { - QListWidgetItem *item = new QListWidgetItem(file.fileName()); - item->setData(Qt::UserRole, file.absoluteFilePath()); - ui->savegameList->addItem(item); - } -} - - -static bool BySortValue(const std::pair &LHS, const std::pair &RHS) -{ - return LHS.first < RHS.first; -} - -template -static QStringList toStringList(InputIterator current, InputIterator end) -{ - QStringList result; - for (; current != end; ++current) { - result.append(*current); - } - return result; -} - -void MainWindow::updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives) -{ - m_DefaultArchives = defaultArchives; - ui->bsaList->clear(); - ui->bsaList->header()->setSectionResizeMode(QHeaderView::ResizeToContents); - std::vector> items; - - BSAInvalidation * invalidation = m_OrganizerCore.managedGame()->feature(); - std::vector files = m_OrganizerCore.directoryStructure()->getFiles(); - - QStringList plugins = m_OrganizerCore.findFiles("", [](const QString &fileName) -> bool { - return fileName.endsWith(".esp", Qt::CaseInsensitive) - || fileName.endsWith(".esm", Qt::CaseInsensitive) - || fileName.endsWith(".esl", Qt::CaseInsensitive); - }); - - auto hasAssociatedPlugin = [&](const QString &bsaName) -> bool { - for (const QString &pluginName : plugins) { - QFileInfo pluginInfo(pluginName); - if (bsaName.startsWith(QFileInfo(pluginName).baseName(), Qt::CaseInsensitive) - && (m_OrganizerCore.pluginList()->state(pluginInfo.fileName()) == IPluginList::STATE_ACTIVE)) { - return true; - } - } - return false; - }; - - for (FileEntry::Ptr current : files) { - QFileInfo fileInfo(ToQString(current->getName().c_str())); - - if (fileInfo.suffix().toLower() == "bsa" || fileInfo.suffix().toLower() == "ba2") { - int index = activeArchives.indexOf(fileInfo.fileName()); - if (index == -1) { - index = 0xFFFF; - } - else { - index += 2; - } - - if ((invalidation != nullptr) && invalidation->isInvalidationBSA(fileInfo.fileName())) { - index = 1; - } - - int originId = current->getOrigin(); - FilesOrigin & origin = m_OrganizerCore.directoryStructure()->getOriginByID(originId); - - QTreeWidgetItem * newItem = new QTreeWidgetItem(QStringList() - << fileInfo.fileName() - << ToQString(origin.getName())); - newItem->setData(0, Qt::UserRole, index); - newItem->setData(1, Qt::UserRole, originId); - newItem->setFlags(newItem->flags() & ~(Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable)); - newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked); - newItem->setData(0, Qt::UserRole, false); - if (m_OrganizerCore.settings().forceEnableCoreFiles() - && defaultArchives.contains(fileInfo.fileName())) { - newItem->setCheckState(0, Qt::Checked); - newItem->setDisabled(true); - newItem->setData(0, Qt::UserRole, true); - } else if (fileInfo.fileName().compare("update.bsa", Qt::CaseInsensitive) == 0) { - newItem->setCheckState(0, Qt::Checked); - newItem->setDisabled(true); - } else if (hasAssociatedPlugin(fileInfo.fileName())) { - newItem->setCheckState(0, Qt::Checked); - newItem->setDisabled(true); - } else { - newItem->setCheckState(0, Qt::Unchecked); - newItem->setDisabled(true); - } - if (index < 0) index = 0; - - UINT32 sortValue = ((origin.getPriority() & 0xFFFF) << 16) | (index & 0xFFFF); - items.push_back(std::make_pair(sortValue, newItem)); - } - } - std::sort(items.begin(), items.end(), BySortValue); - - for (auto iter = items.begin(); iter != items.end(); ++iter) { - int originID = iter->second->data(1, Qt::UserRole).toInt(); - - FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID); - QString modName("data"); - unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName())); - if (modIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - modName = modInfo->name(); - } - QList items = ui->bsaList->findItems(modName, Qt::MatchFixedString); - QTreeWidgetItem * subItem = nullptr; - if (items.length() > 0) { - subItem = items.at(0); - } - else { - subItem = new QTreeWidgetItem(QStringList(modName)); - subItem->setFlags(subItem->flags() & ~Qt::ItemIsDragEnabled); - ui->bsaList->addTopLevelItem(subItem); - } - subItem->addChild(iter->second); - subItem->setExpanded(true); - } - checkBSAList(); -} - -void MainWindow::checkBSAList() -{ - DataArchives * archives = m_OrganizerCore.managedGame()->feature(); - - if (archives != nullptr) { - ui->bsaList->blockSignals(true); - ON_BLOCK_EXIT([&]() { ui->bsaList->blockSignals(false); }); - - QStringList defaultArchives = archives->archives(m_OrganizerCore.currentProfile()); - - bool warning = false; - - for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { - bool modWarning = false; - QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i); - for (int j = 0; j < tlItem->childCount(); ++j) { - QTreeWidgetItem * item = tlItem->child(j); - QString filename = item->text(0); - item->setIcon(0, QIcon()); - item->setToolTip(0, QString()); - - if (item->checkState(0) == Qt::Unchecked) { - if (defaultArchives.contains(filename)) { - item->setIcon(0, QIcon(":/MO/gui/warning")); - item->setToolTip(0, tr("This bsa is enabled in the ini file so it may be required!")); - modWarning = true; - } - } - } - if (modWarning) { - ui->bsaList->expandItem(ui->bsaList->topLevelItem(i)); - warning = true; - } - } - if (warning) { - ui->tabWidget->setTabIcon(1, QIcon(":/MO/gui/warning")); - } else { - ui->tabWidget->setTabIcon(1, QIcon()); - } - } -} - -void MainWindow::saveModMetas() -{ - if (m_MetaSave.isFinished()) { - m_MetaSave = QtConcurrent::run([this]() { - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - modInfo->saveMeta(); - } - }); - } -} - -void MainWindow::fixCategories() -{ - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - std::set categories = modInfo->getCategories(); - for (std::set::iterator iter = categories.begin(); - iter != categories.end(); ++iter) { - if (!m_CategoryFactory.categoryExists(*iter)) { - modInfo->setCategory(*iter, false); - } - } - } -} - - -void MainWindow::setupNetworkProxy(bool activate) -{ - QNetworkProxyFactory::setUseSystemConfiguration(activate); -/* QNetworkProxyQuery query(QUrl("http://www.google.com"), QNetworkProxyQuery::UrlRequest); - query.setProtocolTag("http"); - QList proxies = QNetworkProxyFactory::systemProxyForQuery(query); - if ((proxies.size() > 0) && (proxies.at(0).type() != QNetworkProxy::NoProxy)) { - qDebug("Using proxy: %s", qUtf8Printable(proxies.at(0).hostName())); - QNetworkProxy::setApplicationProxy(proxies[0]); - } else { - qDebug("Not using proxy"); - }*/ -} - - -void MainWindow::activateProxy(bool activate) -{ - QProgressDialog busyDialog(tr("Activating Network Proxy"), QString(), 0, 0, parentWidget()); - busyDialog.setWindowFlags(busyDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); - busyDialog.setWindowModality(Qt::WindowModal); - busyDialog.show(); - QFuture future = QtConcurrent::run(MainWindow::setupNetworkProxy, activate); - while (!future.isFinished()) { - QCoreApplication::processEvents(); - ::Sleep(100); - } - busyDialog.hide(); -} - -void MainWindow::readSettings() -{ - QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat); - - if (settings.contains("window_geometry")) { - restoreGeometry(settings.value("window_geometry").toByteArray()); - } - - if (settings.contains("window_split")) { - ui->splitter->restoreState(settings.value("window_split").toByteArray()); - } - - if (settings.contains("log_split")) { - ui->topLevelSplitter->restoreState(settings.value("log_split").toByteArray()); - } - - bool filtersVisible = settings.value("filters_visible", false).toBool(); - setCategoryListVisible(filtersVisible); - ui->displayCategoriesBtn->setChecked(filtersVisible); - - int selectedExecutable = settings.value("selected_executable").toInt(); - setExecutableIndex(selectedExecutable); - - if (settings.value("Settings/use_proxy", false).toBool()) { - activateProxy(true); - } -} - -void MainWindow::processUpdates() { - QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat); - QVersionNumber lastVersion = QVersionNumber::fromString(settings.value("version", "2.1.2").toString()).normalized(); - QVersionNumber currentVersion = QVersionNumber::fromString(m_OrganizerCore.getVersion().displayString()).normalized(); - if (!m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) { - if (lastVersion < QVersionNumber(2, 1, 3)) { - bool lastHidden = true; - for (int i = ModList::COL_GAME; i < ui->modList->model()->columnCount(); ++i) { - bool hidden = ui->modList->header()->isSectionHidden(i); - ui->modList->header()->setSectionHidden(i, lastHidden); - lastHidden = hidden; - } - } - if (lastVersion < QVersionNumber(2,1,6)) { - ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true); - } - } - - if (currentVersion > lastVersion) { - //NOP - } else if (currentVersion < lastVersion) - qWarning() << tr("Notice: Your current MO version (%1) is lower than the previously used one (%2). " - "The GUI may not downgrade gracefully, so you may experience oddities. " - "However, there should be no serious issues.").arg(currentVersion.toString()).arg(lastVersion.toString()).toStdWString(); - //save version in all case - settings.setValue("version", currentVersion.toString()); -} - -void MainWindow::storeSettings(QSettings &settings) { - settings.setValue("group_state", ui->groupCombo->currentIndex()); - settings.setValue("selected_executable", - ui->executablesListBox->currentIndex()); - - if (settings.value("reset_geometry", false).toBool()) { - settings.remove("window_geometry"); - settings.remove("window_split"); - settings.remove("log_split"); - settings.remove("filters_visible"); - settings.remove("browser_geometry"); - settings.remove("geometry"); - settings.remove("reset_geometry"); - } else { - settings.setValue("window_geometry", saveGeometry()); - settings.setValue("window_split", ui->splitter->saveState()); - settings.setValue("log_split", ui->topLevelSplitter->saveState()); - settings.setValue("browser_geometry", m_IntegratedBrowser.saveGeometry()); - settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked()); - for (const std::pair kv : m_PersistedGeometry) { - QString key = QString("geometry/") + kv.first; - settings.setValue(key, kv.second->saveState()); - } - } -} - -ILockedWaitingForProcess* MainWindow::lock() -{ - if (m_LockDialog != nullptr) { - ++m_LockCount; - return m_LockDialog; - } - if (m_closing) - m_LockDialog = new WaitingOnCloseDialog(this); - else - m_LockDialog = new LockedDialog(this, true); - m_LockDialog->setModal(true); - m_LockDialog->show(); - setEnabled(false); - m_LockDialog->setEnabled(true); //What's the point otherwise? - ++m_LockCount; - return m_LockDialog; -} - -void MainWindow::unlock() -{ - //If you come through here with a null lock pointer, it's a bug! - if (m_LockDialog == nullptr) { - qDebug("Unlocking main window when already unlocked"); - return; - } - --m_LockCount; - if (m_LockCount == 0) { - if (m_closing && m_LockDialog->canceled()) - m_closing = false; - m_LockDialog->hide(); - m_LockDialog->deleteLater(); - m_LockDialog = nullptr; - setEnabled(true); - } -} - -void MainWindow::on_btnRefreshData_clicked() -{ - m_OrganizerCore.refreshDirectoryStructure(); -} - -void MainWindow::on_btnRefreshDownloads_clicked() -{ - m_OrganizerCore.downloadManager()->refreshList(); -} - -void MainWindow::on_tabWidget_currentChanged(int index) -{ - if (index == 0) { - m_OrganizerCore.refreshESPList(); - } else if (index == 1) { - m_OrganizerCore.refreshBSAList(); - } else if (index == 2) { - refreshDataTreeKeepExpandedNodes(); - } else if (index == 3) { - refreshSaveList(); - } -} - - -void MainWindow::installMod(QString fileName) -{ - try { - if (fileName.isEmpty()) { - QStringList extensions = m_OrganizerCore.installationManager()->getSupportedExtensions(); - for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) { - *iter = "*." + *iter; - } - - fileName = FileDialogMemory::getOpenFileName("installMod", this, tr("Choose Mod"), QString(), - tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" ")))); - } - - if (fileName.isEmpty()) { - return; - } else { - m_OrganizerCore.installMod(fileName, QString()); - } - } catch (const std::exception &e) { - reportError(e.what()); - } -} - -void MainWindow::on_startButton_clicked() { - ui->startButton->setEnabled(false); - try { - const Executable &selectedExecutable(getSelectedExecutable()); - QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.m_Title).toString(); - auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.m_Title); - if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.m_Title)) { - forcedLibraries.clear(); - } - m_OrganizerCore.spawnBinary( - selectedExecutable.m_BinaryInfo, selectedExecutable.m_Arguments, - selectedExecutable.m_WorkingDirectory.length() != 0 - ? selectedExecutable.m_WorkingDirectory - : selectedExecutable.m_BinaryInfo.absolutePath(), - selectedExecutable.m_SteamAppID, - customOverwrite, - forcedLibraries); - } catch (...) { - ui->startButton->setEnabled(true); - throw; - } - ui->startButton->setEnabled(true); -} - -static HRESULT CreateShortcut(LPCWSTR targetFileName, LPCWSTR arguments, - LPCSTR linkFileName, LPCWSTR description, - LPCTSTR iconFileName, int iconNumber, - LPCWSTR currentDirectory) -{ - HRESULT result = E_INVALIDARG; - if ((targetFileName != nullptr) && (wcslen(targetFileName) > 0) && - (arguments != nullptr) && - (linkFileName != nullptr) && (strlen(linkFileName) > 0) && - (description != nullptr) && - (currentDirectory != nullptr)) { - - IShellLink* shellLink; - result = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, - IID_IShellLink, (LPVOID*)&shellLink); - - if (!SUCCEEDED(result)) { - qCritical("failed to create IShellLink instance"); - return result; - } - - result = shellLink->SetPath(targetFileName); - if (!SUCCEEDED(result)) { - qCritical("failed to set target path %ls", targetFileName); - shellLink->Release(); - return result; - } - - result = shellLink->SetArguments(arguments); - if (!SUCCEEDED(result)) { - qCritical("failed to set arguments: %ls", arguments); - shellLink->Release(); - return result; - } - - if (wcslen(description) > 0) { - result = shellLink->SetDescription(description); - if (!SUCCEEDED(result)) { - qCritical("failed to set description: %ls", description); - shellLink->Release(); - return result; - } - } - - if (wcslen(currentDirectory) > 0) { - result = shellLink->SetWorkingDirectory(currentDirectory); - if (!SUCCEEDED(result)) { - qCritical("failed to set working directory: %ls", currentDirectory); - shellLink->Release(); - return result; - } - } - - if (iconFileName != nullptr) { - result = shellLink->SetIconLocation(iconFileName, iconNumber); - if (!SUCCEEDED(result)) { - qCritical("failed to load program icon: %ls %d", iconFileName, iconNumber); - shellLink->Release(); - return result; - } - } - - IPersistFile *persistFile; - result = shellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); - if (SUCCEEDED(result)) { - wchar_t linkFileNameW[MAX_PATH]; - if (MultiByteToWideChar(CP_ACP, 0, linkFileName, -1, linkFileNameW, MAX_PATH) > 0) { - result = persistFile->Save(linkFileNameW, TRUE); - } else { - qCritical("failed to create link: %s", linkFileName); - } - persistFile->Release(); - } else { - qCritical("failed to create IPersistFile instance"); - } - - shellLink->Release(); - } - return result; -} - - -bool MainWindow::modifyExecutablesDialog() -{ - bool result = false; - try { - EditExecutablesDialog dialog(*m_OrganizerCore.executablesList(), - *m_OrganizerCore.modList(), - m_OrganizerCore.currentProfile(), - m_OrganizerCore.managedGame()); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog.objectName()); - if (settings.contains(key)) { - dialog.restoreGeometry(settings.value(key).toByteArray()); - } - if (dialog.exec() == QDialog::Accepted) { - m_OrganizerCore.setExecutablesList(dialog.getExecutablesList()); - result = true; - } - settings.setValue(key, dialog.saveGeometry()); - refreshExecutablesList(); - } catch (const std::exception &e) { - reportError(e.what()); - } - return result; -} - -void MainWindow::on_executablesListBox_currentIndexChanged(int index) -{ - QComboBox* executablesList = findChild("executablesListBox"); - - int previousIndex = m_OldExecutableIndex; - m_OldExecutableIndex = index; - - if (executablesList->isEnabled()) { - //I think the 2nd test is impossible - if ((index == 0) || (index > static_cast(m_OrganizerCore.executablesList()->size()))) { - if (modifyExecutablesDialog()) { - setExecutableIndex(previousIndex); - } - } else { - setExecutableIndex(index); - } - } -} - -void MainWindow::helpTriggered() -{ - QWhatsThis::enterWhatsThisMode(); -} - -void MainWindow::wikiTriggered() -{ - QDesktopServices::openUrl(QUrl("https://modorganizer2.github.io/")); -} - -void MainWindow::discordTriggered() -{ - QDesktopServices::openUrl(QUrl("https://discord.gg/cYwdcxj")); -} - -void MainWindow::issueTriggered() -{ - QDesktopServices::openUrl(QUrl("https://github.com/Modorganizer2/modorganizer/issues")); -} - -void MainWindow::tutorialTriggered() -{ - QAction *tutorialAction = qobject_cast(sender()); - if (tutorialAction != nullptr) { - if (QMessageBox::question(this, tr("Start Tutorial?"), - tr("You're about to start a tutorial. For technical reasons it's not possible to end " - "the tutorial early. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - TutorialManager::instance().activateTutorial("MainWindow", tutorialAction->data().toString()); - } - } -} - - -void MainWindow::on_actionInstallMod_triggered() -{ - installMod(); -} - -void MainWindow::on_actionAdd_Profile_triggered() -{ - for (;;) { - ProfilesDialog profilesDialog(m_OrganizerCore.currentProfile()->name(), - m_OrganizerCore.managedGame(), - this); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(profilesDialog.objectName()); - if (settings.contains(key)) { - profilesDialog.restoreGeometry(settings.value(key).toByteArray()); - } - // workaround: need to disable monitoring of the saves directory, otherwise the active - // profile directory is locked - stopMonitorSaves(); - profilesDialog.exec(); - settings.setValue(key, profilesDialog.saveGeometry()); - refreshSaveList(); // since the save list may now be outdated we have to refresh it completely - if (refreshProfiles() && !profilesDialog.failed()) { - break; - } - } - - LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature(); - if (saveGames != nullptr) { - if (saveGames->prepareProfile(m_OrganizerCore.currentProfile())) - refreshSaveList(); - } - - BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature(); - if (invalidation != nullptr) { - if (invalidation->prepareProfile(m_OrganizerCore.currentProfile())) - QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh())); - } -} - -void MainWindow::on_actionModify_Executables_triggered() -{ - if (modifyExecutablesDialog()) { - setExecutableIndex(m_OldExecutableIndex); - } -} - - -void MainWindow::setModListSorting(int index) -{ - Qt::SortOrder order = ((index & 0x01) != 0) ? Qt::DescendingOrder : Qt::AscendingOrder; - int column = index >> 1; - ui->modList->header()->setSortIndicator(column, order); -} - - -void MainWindow::setESPListSorting(int index) -{ - switch (index) { - case 0: { - ui->espList->header()->setSortIndicator(1, Qt::AscendingOrder); - } break; - case 1: { - ui->espList->header()->setSortIndicator(1, Qt::DescendingOrder); - } break; - case 2: { - ui->espList->header()->setSortIndicator(0, Qt::AscendingOrder); - } break; - case 3: { - ui->espList->header()->setSortIndicator(0, Qt::DescendingOrder); - } break; - } -} - -void MainWindow::refresher_progress(int percent) -{ - if (percent == 100) { - m_RefreshProgress->setVisible(false); - statusBar()->hide(); - this->setEnabled(true); - } else if (!m_RefreshProgress->isVisible()) { - this->setEnabled(false); - statusBar()->show(); - m_RefreshProgress->setVisible(true); - m_RefreshProgress->setRange(0, 100); - m_RefreshProgress->setValue(percent); - } -} - -void MainWindow::directory_refreshed() -{ - // some problem-reports may rely on the virtual directory tree so they need to be updated - // now - refreshDataTreeKeepExpandedNodes(); - updateProblemsButton(); - statusBar()->hide(); -} - -void MainWindow::esplist_changed() -{ - updatePluginCount(); -} - -void MainWindow::modorder_changed() -{ - for (unsigned int i = 0; i < m_OrganizerCore.currentProfile()->numMods(); ++i) { - int priority = m_OrganizerCore.currentProfile()->getModPriority(i); - if (m_OrganizerCore.currentProfile()->modEnabled(i)) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - // priorities in the directory structure are one higher because data is 0 - m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->internalName())).setPriority(priority + 1); - } - } - m_OrganizerCore.refreshBSAList(); - m_OrganizerCore.currentProfile()->writeModlist(); - m_ArchiveListWriter.write(); - m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins(); - - { // refresh selection - QModelIndex current = ui->modList->currentIndex(); - if (current.isValid()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt()); - // clear caches on all mods conflicting with the moved mod - for (int i : modInfo->getModOverwrite()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModOverwritten()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveOverwrite()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveOverwritten()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveLooseOverwrite()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveLooseOverwritten()) { - ModInfo::getByIndex(i)->clearCaches(); - } - // update conflict check on the moved mod - modInfo->doConflictCheck(); - m_OrganizerCore.modList()->setOverwriteMarkers(modInfo->getModOverwrite(), modInfo->getModOverwritten()); - m_OrganizerCore.modList()->setArchiveOverwriteMarkers(modInfo->getModArchiveOverwrite(), modInfo->getModArchiveOverwritten()); - m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(modInfo->getModArchiveLooseOverwrite(), modInfo->getModArchiveLooseOverwritten()); - if (m_ModListSortProxy != nullptr) { - m_ModListSortProxy->invalidate(); - } - ui->modList->verticalScrollBar()->repaint(); - } - } -} - -void MainWindow::modInstalled(const QString &modName) -{ - QModelIndexList posList = - m_OrganizerCore.modList()->match(m_OrganizerCore.modList()->index(0, 0), Qt::DisplayRole, modName); - if (posList.count() == 1) { - ui->modList->scrollTo(posList.at(0)); - } -} - -void MainWindow::procError(QProcess::ProcessError error) -{ - reportError(tr("failed to spawn notepad.exe: %1").arg(error)); - this->sender()->deleteLater(); -} - -void MainWindow::procFinished(int, QProcess::ExitStatus) -{ - this->sender()->deleteLater(); -} - -void MainWindow::showMessage(const QString &message) -{ - MessageDialog::showMessage(message, this); -} - -void MainWindow::showError(const QString &message) -{ - reportError(message); -} - -void MainWindow::installMod_clicked() -{ - installMod(); -} - -void MainWindow::modRenamed(const QString &oldName, const QString &newName) -{ - Profile::renameModInAllProfiles(oldName, newName); - - // immediately refresh the active profile because the data in memory is invalid - m_OrganizerCore.currentProfile()->refreshModStatus(); - - // also fix the directory structure - try { - if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldName))) { - FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldName)); - origin.setName(ToWString(newName)); - } else { - - } - } catch (const std::exception &e) { - reportError(tr("failed to change origin name: %1").arg(e.what())); - } -} - -void MainWindow::fileMoved(const QString &filePath, const QString &oldOriginName, const QString &newOriginName) -{ - const FileEntry::Ptr filePtr = m_OrganizerCore.directoryStructure()->findFile(ToWString(filePath)); - if (filePtr.get() != nullptr) { - try { - if (m_OrganizerCore.directoryStructure()->originExists(ToWString(newOriginName))) { - FilesOrigin &newOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(newOriginName)); - - QString fullNewPath = ToQString(newOrigin.getPath()) + "\\" + filePath; - WIN32_FIND_DATAW findData; - HANDLE hFind; - hFind = ::FindFirstFileW(ToWString(fullNewPath).c_str(), &findData); - filePtr->addOrigin(newOrigin.getID(), findData.ftCreationTime, L"", -1); - FindClose(hFind); - } - if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldOriginName))) { - FilesOrigin &oldOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldOriginName)); - filePtr->removeOrigin(oldOrigin.getID()); - } - } catch (const std::exception &e) { - reportError(tr("failed to move \"%1\" from mod \"%2\" to \"%3\": %4").arg(filePath).arg(oldOriginName).arg(newOriginName).arg(e.what())); - } - } else { - // this is probably not an error, the specified path is likely a directory - } -} - -QTreeWidgetItem *MainWindow::addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type) -{ - QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name)); - item->setData(0, Qt::ToolTipRole, name); - item->setData(0, Qt::UserRole, categoryID); - item->setData(0, Qt::UserRole + 1, type); - if (root != nullptr) { - root->addChild(item); - } else { - ui->categoriesList->addTopLevelItem(item); - } - return item; -} - -void MainWindow::addContentFilters() -{ - for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) { - addFilterItem(nullptr, tr("").arg(ModInfo::getContentTypeName(i)), i, ModListSortProxy::TYPE_CONTENT); - } -} - -void MainWindow::addCategoryFilters(QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID) -{ - for (unsigned int i = 1; - i < static_cast(m_CategoryFactory.numCategories()); ++i) { - if ((m_CategoryFactory.getParentID(i) == targetID)) { - int categoryID = m_CategoryFactory.getCategoryID(i); - if (categoriesUsed.find(categoryID) != categoriesUsed.end()) { - QTreeWidgetItem *item = - addFilterItem(root, m_CategoryFactory.getCategoryName(i), - categoryID, ModListSortProxy::TYPE_CATEGORY); - if (m_CategoryFactory.hasChildren(i)) { - addCategoryFilters(item, categoriesUsed, categoryID); - } - } - } - } -} - -void MainWindow::refreshFilters() -{ - QItemSelection currentSelection = ui->modList->selectionModel()->selection(); - - QVariant currentIndexName = ui->modList->currentIndex().data(); - ui->modList->setCurrentIndex(QModelIndex()); - - QStringList selectedItems; - for (QTreeWidgetItem *item : ui->categoriesList->selectedItems()) { - selectedItems.append(item->text(0)); - } - - ui->categoriesList->clear(); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CHECKED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_BACKUP, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_MANAGED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNMANAGED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CONFLICT, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED, ModListSortProxy::TYPE_SPECIAL); - - addContentFilters(); - std::set categoriesUsed; - for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx); - for (int categoryID : modInfo->getCategories()) { - int currentID = categoryID; - std::set cycleTest; - // also add parents so they show up in the tree - while (currentID != 0) { - categoriesUsed.insert(currentID); - if (!cycleTest.insert(currentID).second) { - qWarning("cycle in categories: %s", qUtf8Printable(SetJoin(cycleTest, ", "))); - break; - } - currentID = m_CategoryFactory.getParentID(m_CategoryFactory.getCategoryIndex(currentID)); - } - } - } - - addCategoryFilters(nullptr, categoriesUsed, 0); - - for (const QString &item : selectedItems) { - QList matches = ui->categoriesList->findItems(item, Qt::MatchFixedString | Qt::MatchRecursive); - if (matches.size() > 0) { - matches.at(0)->setSelected(true); - } - } - ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select); - QModelIndexList matchList; - if (currentIndexName.isValid()) { - matchList = ui->modList->model()->match(ui->modList->model()->index(0, 0), Qt::DisplayRole, currentIndexName); - } - - if (matchList.size() > 0) { - ui->modList->setCurrentIndex(matchList.at(0)); - } -} - - -void MainWindow::renameMod_clicked() -{ - try { - ui->modList->edit(ui->modList->currentIndex()); - } catch (const std::exception &e) { - reportError(tr("failed to rename mod: %1").arg(e.what())); - } -} - - -void MainWindow::restoreBackup_clicked() -{ - QRegExp backupRegEx("(.*)_backup[0-9]*$"); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - if (backupRegEx.indexIn(modInfo->name()) != -1) { - QString regName = backupRegEx.cap(1); - QDir modDir(QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory())); - if (!modDir.exists(regName) || - (QMessageBox::question(this, tr("Overwrite?"), - tr("This will replace the existing mod \"%1\". Continue?").arg(regName), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) { - reportError(tr("failed to remove mod \"%1\"").arg(regName)); - } else { - QString destinationPath = QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()) + "/" + regName; - if (!modDir.rename(modInfo->absolutePath(), destinationPath)) { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath)); - } - m_OrganizerCore.refreshModList(); - } - } - } -} - -void MainWindow::modlistChanged(const QModelIndex&, int) -{ - m_OrganizerCore.currentProfile()->writeModlist(); - updateModCount(); -} - -void MainWindow::modlistChanged(const QModelIndexList&, int) -{ - m_OrganizerCore.currentProfile()->writeModlist(); - updateModCount(); -} - -void MainWindow::modlistSelectionChanged(const QModelIndex ¤t, const QModelIndex&) -{ - if (current.isValid()) { - ModInfo::Ptr selectedMod = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.modList()->setOverwriteMarkers(selectedMod->getModOverwrite(), selectedMod->getModOverwritten()); - m_OrganizerCore.modList()->setArchiveOverwriteMarkers(selectedMod->getModArchiveOverwrite(), selectedMod->getModArchiveOverwritten()); - m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(selectedMod->getModArchiveLooseOverwrite(), selectedMod->getModArchiveLooseOverwritten()); - } else { - m_OrganizerCore.modList()->setOverwriteMarkers(std::set(), std::set()); - m_OrganizerCore.modList()->setArchiveOverwriteMarkers(std::set(), std::set()); - m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(std::set(), std::set()); - } -/* if ((m_ModListSortProxy != nullptr) - && !m_ModListSortProxy->beingInvalidated()) { - m_ModListSortProxy->invalidate(); - }*/ - ui->modList->verticalScrollBar()->repaint(); -} - -void MainWindow::modlistSelectionsChanged(const QItemSelection &selected) -{ - m_OrganizerCore.pluginList()->highlightPlugins(ui->modList->selectionModel(), *m_OrganizerCore.directoryStructure(), *m_OrganizerCore.currentProfile()); - ui->espList->verticalScrollBar()->repaint(); -} - -void MainWindow::esplistSelectionsChanged(const QItemSelection &selected) -{ - m_OrganizerCore.modList()->highlightMods(ui->espList->selectionModel(), *m_OrganizerCore.directoryStructure()); - ui->modList->verticalScrollBar()->repaint(); -} - -void MainWindow::modListSortIndicatorChanged(int, Qt::SortOrder) -{ - ui->modList->verticalScrollBar()->repaint(); -} - -void MainWindow::modListSectionResized(int logicalIndex, int oldSize, int newSize) -{ - bool enabled = (newSize != 0); - qobject_cast(ui->modList->model())->setColumnVisible(logicalIndex, enabled); -} - -void MainWindow::removeMod_clicked() -{ - try { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - QString mods; - QStringList modNames; - for (QModelIndex idx : selection->selectedRows()) { - QString name = idx.data().toString(); - if (!ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->isRegular()) { - continue; - } - mods += "
  • " + name + "
  • "; - modNames.append(ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->name()); - } - if (QMessageBox::question(this, tr("Confirm"), - tr("Remove the following mods?
      %1
    ").arg(mods), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - // use mod names instead of indexes because those become invalid during the removal - DownloadManager::startDisableDirWatcher(); - for (QString name : modNames) { - m_OrganizerCore.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex()); - } - DownloadManager::endDisableDirWatcher(); - } - } else { - m_OrganizerCore.modList()->removeRow(m_ContextRow, QModelIndex()); - } - updateModCount(); - updatePluginCount(); - } catch (const std::exception &e) { - reportError(tr("failed to remove mod: %1").arg(e.what())); - } -} - - -void MainWindow::modRemoved(const QString &fileName) -{ - if (!fileName.isEmpty() && !QFileInfo(fileName).isAbsolute()) { - m_OrganizerCore.downloadManager()->markUninstalled(fileName); - } -} - - -void MainWindow::reinstallMod_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QString installationFile = modInfo->getInstallationFile(); - if (installationFile.length() != 0) { - QString fullInstallationFile; - QFileInfo fileInfo(installationFile); - if (fileInfo.isAbsolute()) { - if (fileInfo.exists()) { - fullInstallationFile = installationFile; - } else { - fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + fileInfo.fileName(); - } - } else { - fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + installationFile; - } - if (QFile::exists(fullInstallationFile)) { - m_OrganizerCore.installMod(fullInstallationFile, modInfo->name()); - } else { - QMessageBox::information(this, tr("Failed"), tr("Installation file no longer exists")); - } - } else { - QMessageBox::information(this, tr("Failed"), - tr("Mods installed with old versions of MO can't be reinstalled in this way.")); - } -} - -void MainWindow::backupMod_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QString backupDirectory = m_OrganizerCore.installationManager()->generateBackupName(modInfo->absolutePath()); - if (!copyDir(modInfo->absolutePath(), backupDirectory, false)) { - QMessageBox::information(this, tr("Failed"), - tr("Failed to create backup.")); - } - m_OrganizerCore.refreshModList(); -} - -void MainWindow::resumeDownload(int downloadIndex) -{ - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { - m_OrganizerCore.downloadManager()->resumeDownload(downloadIndex); - } else { - QString username, password; - if (m_OrganizerCore.settings().getNexusLogin(username, password)) { - m_OrganizerCore.doAfterLogin([this, downloadIndex] () { - this->resumeDownload(downloadIndex); - }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to resume a download"), this); - } - } -} - - -void MainWindow::endorseMod(ModInfo::Ptr mod) -{ - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { - mod->endorse(true); - } else { - QString username, password; - if (m_OrganizerCore.settings().getNexusLogin(username, password)) { - m_OrganizerCore.doAfterLogin(boost::bind(&MainWindow::endorseMod, this, mod)); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - } - } -} - - -void MainWindow::endorse_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { - MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(true); - } - } - else { - QString username, password; - if (m_OrganizerCore.settings().getNexusLogin(username, password)) { - MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.doAfterLogin(boost::bind(&MainWindow::endorseMod, this, modInfo)); - } - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - return; - } - } - } - else { - endorseMod(ModInfo::getByIndex(m_ContextRow)); - } -} - -void MainWindow::dontendorse_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->setNeverEndorse(); - } - } - else { - ModInfo::getByIndex(m_ContextRow)->setNeverEndorse(); - } -} - - -void MainWindow::unendorseMod(ModInfo::Ptr mod) -{ - QString username, password; - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { - ModInfo::getByIndex(m_ContextRow)->endorse(false); - } else { - if (m_OrganizerCore.settings().getNexusLogin(username, password)) { - m_OrganizerCore.doAfterLogin([this] () { this->unendorse_clicked(); }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - } - } -} - - -void MainWindow::unendorse_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { - MessageDialog::showMessage(tr("Unendorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(false); - } - } - else { - QString username, password; - if (m_OrganizerCore.settings().getNexusLogin(username, password)) { - MessageDialog::showMessage(tr("Unendorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.doAfterLogin(boost::bind(&MainWindow::unendorseMod, this, modInfo)); - } - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - return; - } - } - } - else { - unendorseMod(ModInfo::getByIndex(m_ContextRow)); - } -} - -void MainWindow::loginFailed(const QString &error) -{ - qDebug("login failed: %s", qUtf8Printable(error)); - statusBar()->hide(); -} - -void MainWindow::windowTutorialFinished(const QString &windowName) -{ - m_OrganizerCore.settings().directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true); -} - -void MainWindow::overwriteClosed(int) -{ - OverwriteInfoDialog *dialog = this->findChild("__overwriteDialog"); - if (dialog != nullptr) { - m_OrganizerCore.modList()->modInfoChanged(dialog->modInfo()); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog->objectName()); - settings.setValue(key, dialog->saveGeometry()); - dialog->deleteLater(); - } - m_OrganizerCore.refreshDirectoryStructure(); -} - - -void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab) -{ - if (!m_OrganizerCore.modList()->modInfoAboutToChange(modInfo)) { - qDebug("A different mod information dialog is open. If this is incorrect, please restart MO"); - return; - } - std::vector flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { - QDialog *dialog = this->findChild("__overwriteDialog"); - try { - if (dialog == nullptr) { - dialog = new OverwriteInfoDialog(modInfo, this); - dialog->setObjectName("__overwriteDialog"); - } else { - qobject_cast(dialog)->setModInfo(modInfo); - } - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog->objectName()); - if (settings.contains(key)) { - dialog->restoreGeometry(settings.value(key).toByteArray()); - } - dialog->show(); - dialog->raise(); - dialog->activateWindow(); - connect(dialog, SIGNAL(finished(int)), this, SLOT(overwriteClosed(int))); - } catch (const std::exception &e) { - reportError(tr("Failed to display overwrite dialog: %1").arg(e.what())); - } - } else { - modInfo->saveMeta(); - ModInfoDialog dialog(modInfo, m_OrganizerCore.directoryStructure(), modInfo->hasFlag(ModInfo::FLAG_FOREIGN), &m_OrganizerCore, &m_PluginContainer, this); - connect(&dialog, SIGNAL(linkActivated(QString)), this, SLOT(linkClicked(QString))); - connect(&dialog, SIGNAL(downloadRequest(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); - connect(&dialog, SIGNAL(modOpen(QString, int)), this, SLOT(displayModInformation(QString, int)), Qt::QueuedConnection); - connect(&dialog, SIGNAL(modOpenNext(int)), this, SLOT(modOpenNext(int)), Qt::QueuedConnection); - connect(&dialog, SIGNAL(modOpenPrev(int)), this, SLOT(modOpenPrev(int)), Qt::QueuedConnection); - connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); - connect(&dialog, SIGNAL(endorseMod(ModInfo::Ptr)), this, SLOT(endorseMod(ModInfo::Ptr))); - - //Open the tab first if we want to use the standard indexes of the tabs. - if (tab != -1) { - dialog.openTab(tab); - } - - dialog.restoreTabState(m_OrganizerCore.settings().directInterface().value("mod_info_tabs").toByteArray()); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog.objectName()); - if (settings.contains(key)) { - dialog.restoreGeometry(settings.value(key).toByteArray()); - } - - //If no tab was specified use the first tab from the left based on the user order. - if (tab == -1) { - for (int i = 0; i < dialog.findChild("tabWidget")->count(); ++i) { - if (dialog.findChild("tabWidget")->isTabEnabled(i)) { - dialog.findChild("tabWidget")->setCurrentIndex(i); - break; - } - } - } - - dialog.exec(); - m_OrganizerCore.settings().directInterface().setValue("mod_info_tabs", dialog.saveTabState()); - settings.setValue(key, dialog.saveGeometry()); - - modInfo->saveMeta(); - emit modInfoDisplayed(); - m_OrganizerCore.modList()->modInfoChanged(modInfo); - } - - if (m_OrganizerCore.currentProfile()->modEnabled(index) - && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { - FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - - if (m_OrganizerCore.directoryStructure()->originExists(ToWString(modInfo->name()))) { - FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - - m_OrganizerCore.directoryRefresher()->addModToStructure(m_OrganizerCore.directoryStructure() - , modInfo->name() - , m_OrganizerCore.currentProfile()->getModPriority(index) - , modInfo->absolutePath() - , modInfo->stealFiles() - , modInfo->archives()); - DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure()); - m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins(); - m_OrganizerCore.refreshLists(); - } - } -} - -bool MainWindow::closeWindow() -{ - return close(); -} - -void MainWindow::setWindowEnabled(bool enabled) -{ - setEnabled(enabled); -} - - -void MainWindow::modOpenNext(int tab) -{ - QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0)); - index = m_ModListSortProxy->index((index.row() + 1) % m_ModListSortProxy->rowCount(), 0); - - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); - std::vector flags = mod->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())) { - // skip overwrite and backups and separators - modOpenNext(tab); - } else { - displayModInformation(m_ContextRow,tab); - } -} - -void MainWindow::modOpenPrev(int tab) -{ - QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0)); - int row = index.row() - 1; - if (row == -1) { - row = m_ModListSortProxy->rowCount() - 1; - } - - m_ContextRow = m_ModListSortProxy->mapToSource(m_ModListSortProxy->index(row, 0)).row(); - ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); - std::vector flags = mod->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())) { - // skip overwrite and backups and separators - modOpenPrev(tab); - } else { - displayModInformation(m_ContextRow,tab); - } -} - -void MainWindow::displayModInformation(const QString &modName, int tab) -{ - unsigned int index = ModInfo::getIndex(modName); - if (index == UINT_MAX) { - qCritical("failed to resolve mod name %s", qUtf8Printable(modName)); - return; - } - - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - displayModInformation(modInfo, index, tab); -} - - -void MainWindow::displayModInformation(int row, int tab) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - displayModInformation(modInfo, row, tab); -} - - -void MainWindow::ignoreMissingData_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - int row_idx = idx.data(Qt::UserRole + 1).toInt(); - ModInfo::Ptr info = ModInfo::getByIndex(row_idx); - //QDir(info->absolutePath()).mkdir("textures"); - info->testValid(); - info->markValidated(true); - connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); - - emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1)); - } - } else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - //QDir(info->absolutePath()).mkdir("textures"); - info->testValid(); - info->markValidated(true); - connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); - - emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); - } -} - -void MainWindow::markConverted_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - int row_idx = idx.data(Qt::UserRole + 1).toInt(); - ModInfo::Ptr info = ModInfo::getByIndex(row_idx); - info->markConverted(true); - connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); - emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1)); - } - } else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->markConverted(true); - connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); - emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); - } -} - - -void MainWindow::visitOnNexus_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - int count = selection->selectedRows().count(); - if (count > 10) { - if (QMessageBox::question(this, tr("Opening Nexus Links"), - tr("You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?").arg(count), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - int row_idx; - ModInfo::Ptr info; - QString gameName; - QString webUrl; - for (QModelIndex idx : selection->selectedRows()) { - row_idx = idx.data(Qt::UserRole + 1).toInt(); - info = ModInfo::getByIndex(row_idx); - int modID = info->getNexusID(); - webUrl = info->getURL(); - gameName = info->getGameName(); - if (modID > 0) { - linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); - } - else if (webUrl != "") { - linkClicked(webUrl); - } - } - } - else { - int modID = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole).toInt(); - QString gameName = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole + 4).toString(); - if (modID > 0) { - linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); - } else { - MessageDialog::showMessage(tr("Nexus ID for this Mod is unknown"), this); - } - } -} - -void MainWindow::visitWebPage_clicked() -{ - - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - int count = selection->selectedRows().count(); - if (count > 10) { - if (QMessageBox::question(this, tr("Opening Web Pages"), - tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(count), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - int row_idx; - ModInfo::Ptr info; - QString gameName; - QString webUrl; - for (QModelIndex idx : selection->selectedRows()) { - row_idx = idx.data(Qt::UserRole + 1).toInt(); - info = ModInfo::getByIndex(row_idx); - int modID = info->getNexusID(); - webUrl = info->getURL(); - gameName = info->getGameName(); - if (modID > 0) { - linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); - } - else if (webUrl != "") { - linkClicked(webUrl); - } - } - } - else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - if (info->getURL() != "") { - linkClicked(info->getURL()); - } - else { - MessageDialog::showMessage(tr("Web page for this mod is unknown"), this); - } - } -} - -void MainWindow::openExplorer_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - ::ShellExecuteW(nullptr, L"explore", ToWString(info->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } - } - else { - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } -} - -void MainWindow::openOriginExplorer_clicked() -{ - QItemSelectionModel *selection = ui->espList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 0) { - for (QModelIndex idx : selection->selectedRows()) { - QString fileName = idx.data().toString(); - unsigned int modIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)); - if (modIndex == UINT_MAX) { - continue; - } - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - std::vector flags = modInfo->getFlags(); - - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } - } - else { - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector flags = modInfo->getFlags(); - - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } -} - -void MainWindow::openExplorer_activated() -{ - if (ui->modList->hasFocus()) { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() == 1 ) { - - QModelIndex idx = selection->currentIndex(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - std::vector flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } - - } - } - - if (ui->espList->hasFocus()) { - QItemSelectionModel *selection = ui->espList->selectionModel(); - - if (selection->hasSelection() && selection->selectedRows().count() == 1) { - - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - - - unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)); - if (modInfoIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); - std::vector flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } - } - } - } -} - -void MainWindow::refreshProfile_activated() -{ - m_OrganizerCore.profileRefresh(); -} - -void MainWindow::search_activated() -{ - if (ui->modList->hasFocus() || ui->modFilterEdit->hasFocus()) { - ui->modFilterEdit->setFocus(); - ui->modFilterEdit->setSelection(0, INT_MAX); - } - - else if (ui->espList->hasFocus() || ui->espFilterEdit->hasFocus()) { - ui->espFilterEdit->setFocus(); - ui->espFilterEdit->setSelection(0, INT_MAX); - } - - else if (ui->downloadView->hasFocus() || ui->downloadFilterEdit->hasFocus()) { - ui->downloadFilterEdit->setFocus(); - ui->downloadFilterEdit->setSelection(0, INT_MAX); - } -} - -void MainWindow::searchClear_activated() -{ - if (ui->modList->hasFocus() || ui->modFilterEdit->hasFocus()) { - ui->modFilterEdit->clear(); - ui->modList->setFocus(); - } - - else if (ui->espList->hasFocus() || ui->espFilterEdit->hasFocus()) { - ui->espFilterEdit->clear(); - ui->espList->setFocus(); - } - - else if (ui->downloadView->hasFocus() || ui->downloadFilterEdit->hasFocus()) { - ui->downloadFilterEdit->clear(); - ui->downloadView->setFocus(); - } -} - -void MainWindow::updateModCount() -{ - int activeCount = 0; - int visActiveCount = 0; - int backupCount = 0; - int visBackupCount = 0; - int foreignCount = 0; - int visForeignCount = 0; - int separatorCount = 0; - int visSeparatorCount = 0; - int regularCount = 0; - int visRegularCount = 0; - - QStringList allMods = m_OrganizerCore.modList()->allMods(); - - auto hasFlag = [](std::vector flags, ModInfo::EFlag filter) { - return std::find(flags.begin(), flags.end(), filter) != flags.end(); - }; - - bool isEnabled; - bool isVisible; - for (QString mod : allMods) { - int modIndex = ModInfo::getIndex(mod); - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - std::vector modFlags = modInfo->getFlags(); - isEnabled = m_OrganizerCore.currentProfile()->modEnabled(modIndex); - isVisible = m_ModListSortProxy->filterMatchesMod(modInfo, isEnabled); - - for (auto flag : modFlags) { - switch (flag) { - case ModInfo::FLAG_BACKUP: backupCount++; - if (isVisible) - visBackupCount++; - break; - case ModInfo::FLAG_FOREIGN: foreignCount++; - if (isVisible) - visForeignCount++; - break; - case ModInfo::FLAG_SEPARATOR: separatorCount++; - if (isVisible) - visSeparatorCount++; - break; - } - } - - if (!hasFlag(modFlags, ModInfo::FLAG_BACKUP) && - !hasFlag(modFlags, ModInfo::FLAG_FOREIGN) && - !hasFlag(modFlags, ModInfo::FLAG_SEPARATOR) && - !hasFlag(modFlags, ModInfo::FLAG_OVERWRITE)) { - if (isEnabled) { - activeCount++; - if (isVisible) - visActiveCount++; - } - if (isVisible) - visRegularCount++; - regularCount++; - } - } - - ui->activeModsCounter->display(visActiveCount); - ui->activeModsCounter->setToolTip(tr("" - "" - "" - "" - "" - "" - "
    TypeAllVisible
    Enabled mods: %1 / %2%3 / %4
    Unmanaged/DLCs: %5%6
    Mod backups: %7%8
    Separators: %9%10
    ") - .arg(activeCount) - .arg(regularCount) - .arg(visActiveCount) - .arg(visRegularCount) - .arg(foreignCount) - .arg(visForeignCount) - .arg(backupCount) - .arg(visBackupCount) - .arg(separatorCount) - .arg(visSeparatorCount) - ); -} - -void MainWindow::updatePluginCount() -{ - int activeMasterCount = 0; - int activeLightMasterCount = 0; - int activeRegularCount = 0; - int masterCount = 0; - int lightMasterCount = 0; - int regularCount = 0; - int activeVisibleCount = 0; - - PluginList *list = m_OrganizerCore.pluginList(); - QString filter = ui->espFilterEdit->text(); - - for (QString plugin : list->pluginNames()) { - bool active = list->isEnabled(plugin); - bool visible = m_PluginListSortProxy->filterMatchesPlugin(plugin); - if (list->isLight(plugin) || list->isLightFlagged(plugin)) { - lightMasterCount++; - activeLightMasterCount += active; - activeVisibleCount += visible && active; - } else if (list->isMaster(plugin)) { - masterCount++; - activeMasterCount += active; - activeVisibleCount += visible && active; - } else { - regularCount++; - activeRegularCount += active; - activeVisibleCount += visible && active; - } - } - - int activeCount = activeMasterCount + activeLightMasterCount + activeRegularCount; - int totalCount = masterCount + lightMasterCount + regularCount; - - ui->activePluginsCounter->display(activeVisibleCount); - ui->activePluginsCounter->setToolTip(tr("" - "" - "" - "" - "" - "" - "" - "
    TypeActive Total
    All plugins:%1 %2
    ESMs:%3 %4
    ESPs:%7 %8
    ESMs+ESPs:%9 %10
    ESLs:%5 %6
    ") - .arg(activeCount).arg(totalCount) - .arg(activeMasterCount).arg(masterCount) - .arg(activeLightMasterCount).arg(lightMasterCount) - .arg(activeRegularCount).arg(regularCount) - .arg(activeMasterCount+activeRegularCount).arg(masterCount+regularCount) - ); -} - -void MainWindow::information_clicked() -{ - try { - displayModInformation(m_ContextRow); - } catch (const std::exception &e) { - reportError(e.what()); - } -} - -void MainWindow::createEmptyMod_clicked() -{ - GuessedValue name; - name.setFilter(&fixDirectoryName); - - while (name->isEmpty()) { - bool ok; - name.update(QInputDialog::getText(this, tr("Create Mod..."), - tr("This will create an empty mod.\n" - "Please enter a name:"), QLineEdit::Normal, "", &ok), - GUESS_USER); - if (!ok) { - return; - } - } - - if (m_OrganizerCore.getMod(name) != nullptr) { - reportError(tr("A mod with this name already exists")); - return; - } - - int newPriority = -1; - if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) { - newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); - } - - IModInterface *newMod = m_OrganizerCore.createMod(name); - if (newMod == nullptr) { - return; - } - - m_OrganizerCore.refreshModList(); - - if (newPriority >= 0) { - m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); - } -} - -void MainWindow::createSeparator_clicked() -{ - GuessedValue name; - name.setFilter(&fixDirectoryName); - while (name->isEmpty()) - { - bool ok; - name.update(QInputDialog::getText(this, tr("Create Separator..."), - tr("This will create a new separator.\n" - "Please enter a name:"), QLineEdit::Normal, "", &ok), - GUESS_USER); - if (!ok) { return; } - } - if (m_OrganizerCore.getMod(name) != nullptr) - { - reportError(tr("A separator with this name already exists")); - return; - } - name->append("_separator"); - if (m_OrganizerCore.getMod(name) != nullptr) - { - return; - } - - int newPriority = -1; - if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) - { - newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); - } - - if (m_OrganizerCore.createMod(name) == nullptr) { return; } - m_OrganizerCore.refreshModList(); - - if (newPriority >= 0) - { - m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); - } - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QColor previousColor = settings.value("previousSeparatorColor", QColor()).value(); - if (previousColor.isValid()) { - ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(previousColor); - } - -} - -void MainWindow::setColor_clicked() -{ - QSettings &settings = m_OrganizerCore.settings().directInterface(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QColorDialog dialog(this); - dialog.setOption(QColorDialog::ShowAlphaChannel); - QColor currentColor = modInfo->getColor(); - QColor previousColor = settings.value("previousSeparatorColor", QColor()).value(); - if (currentColor.isValid()) - dialog.setCurrentColor(currentColor); - else - dialog.setCurrentColor(previousColor); - if (!dialog.exec()) - return; - currentColor = dialog.currentColor(); - if (!currentColor.isValid()) - return; - settings.setValue("previousSeparatorColor", currentColor); - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - auto flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - info->setColor(currentColor); - } - } - } - else { - modInfo->setColor(currentColor); - } -} - -void MainWindow::resetColor_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QColor color = QColor(); - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - auto flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - info->setColor(color); - } - } - } - else { - modInfo->setColor(color); - } - Settings::instance().directInterface().remove("previousSeparatorColor"); -} - -void MainWindow::createModFromOverwrite() -{ - GuessedValue name; - name.setFilter(&fixDirectoryName); - - while (name->isEmpty()) { - bool ok; - name.update(QInputDialog::getText(this, tr("Create Mod..."), - tr("This will move all files from overwrite into a new, regular mod.\n" - "Please enter a name:"), QLineEdit::Normal, "", &ok), - GUESS_USER); - if (!ok) { - return; - } - } - - if (m_OrganizerCore.getMod(name) != nullptr) { - reportError(tr("A mod with this name already exists")); - return; - } - - const IModInterface *newMod = m_OrganizerCore.createMod(name); - if (newMod == nullptr) { - return; - } - - doMoveOverwriteContentToMod(newMod->absolutePath()); -} - -void MainWindow::moveOverwriteContentToExistingMod() -{ - QStringList mods; - auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); - for (auto & iter : indexesByPriority) { - if ((iter.second != UINT_MAX)) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(iter.second); - if (!modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN) && !modInfo->hasFlag(ModInfo::FLAG_OVERWRITE)) { - mods << modInfo->name(); - } - } - } - - ListDialog dialog(this); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog.objectName()); - - dialog.setWindowTitle("Select a mod..."); - dialog.setChoices(mods); - - if (settings.contains(key)) { - dialog.restoreGeometry(settings.value(key).toByteArray()); - } - if (dialog.exec() == QDialog::Accepted) { - - QString result = dialog.getChoice(); - if (!result.isEmpty()) { - - QString modAbsolutePath; - - for (const auto& mod : m_OrganizerCore.modsSortedByProfilePriority()) { - if (result.compare(mod) == 0) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(mod)); - modAbsolutePath = modInfo->absolutePath(); - break; - } - } - - if (modAbsolutePath.isNull()) { - qWarning("Mod %s has not been found, for some reason", qUtf8Printable(result)); - return; - } - - doMoveOverwriteContentToMod(modAbsolutePath); - } - } - settings.setValue(key, dialog.saveGeometry()); -} - -void MainWindow::doMoveOverwriteContentToMod(const QString &modAbsolutePath) -{ - unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { - std::vector flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); - - ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex); - bool successful = shellMove((QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"), - (QDir::toNativeSeparators(modAbsolutePath)), false, this); - - if (successful) { - MessageDialog::showMessage(tr("Move successful."), this); - } - else { - qCritical("Move operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError()))); - } - - m_OrganizerCore.refreshModList(); -} - -void MainWindow::clearOverwrite() -{ - unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { - std::vector flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) - != flags.end(); - }); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); - if (modInfo) - { - QDir overwriteDir(modInfo->absolutePath()); - if (QMessageBox::question(this, tr("Are you sure?"), - tr("About to recursively delete:\n") + overwriteDir.absolutePath(), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) - { - QStringList delList; - for (auto f : overwriteDir.entryList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot)) - delList.push_back(overwriteDir.absoluteFilePath(f)); - shellDelete(delList, true); - updateProblemsButton(); - } - } -} - -void MainWindow::cancelModListEditor() -{ - ui->modList->setEnabled(false); - ui->modList->setEnabled(true); -} - -void MainWindow::on_modList_doubleClicked(const QModelIndex &index) -{ - if (!index.isValid()) { - return; - } - - if (m_OrganizerCore.modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { - // don't interpret double click if we only just checked a mod - return; - } - - QModelIndex sourceIdx = mapToModel(m_OrganizerCore.modList(), index); - if (!sourceIdx.isValid()) { - return; - } - - Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); - if (modifiers.testFlag(Qt::ControlModifier)) { - try { - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - openExplorer_clicked(); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->modList->closePersistentEditor(index); - } - catch (const std::exception &e) { - reportError(e.what()); - } - } - else { - try { - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - sourceIdx.column(); - int tab = -1; - switch (sourceIdx.column()) { - case ModList::COL_NOTES: tab = ModInfoDialog::TAB_NOTES; break; - case ModList::COL_VERSION: tab = ModInfoDialog::TAB_NEXUS; break; - case ModList::COL_MODID: tab = ModInfoDialog::TAB_NEXUS; break; - case ModList::COL_GAME: tab = ModInfoDialog::TAB_NEXUS; break; - case ModList::COL_CATEGORY: tab = ModInfoDialog::TAB_CATEGORIES; break; - case ModList::COL_FLAGS: tab = ModInfoDialog::TAB_CONFLICTS; break; - default: tab = -1; - } - displayModInformation(sourceIdx.row(), tab); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->modList->closePersistentEditor(index); - } - catch (const std::exception &e) { - reportError(e.what()); - } - } -} - -void MainWindow::on_listOptionsBtn_pressed() -{ - m_ContextRow = -1; -} - -void MainWindow::openOriginInformation_clicked() -{ - try { - QItemSelectionModel *selection = ui->espList->selectionModel(); - //we don't want to open multiple modinfodialogs. - /*if (selection->hasSelection() && selection->selectedRows().count() > 0) { - - for (QModelIndex idx : selection->selectedRows()) { - QString fileName = idx.data().toString(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - } - } - } - else {}*/ - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - } - } - catch (const std::exception &e) { - reportError(e.what()); - } -} - -void MainWindow::on_espList_doubleClicked(const QModelIndex &index) -{ - if (!index.isValid()) { - return; - } - - if (m_OrganizerCore.pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { - // don't interpret double click if we only just checked a plugin - return; - } - - QModelIndex sourceIdx = mapToModel(m_OrganizerCore.pluginList(), index); - if (!sourceIdx.isValid()) { - return; - } - try { - - QItemSelectionModel *selection = ui->espList->selectionModel(); - - if (selection->hasSelection() && selection->selectedRows().count() == 1) { - - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - - if (ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)) == UINT_MAX) - return; - - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - - Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); - if (modifiers.testFlag(Qt::ControlModifier)) { - openExplorer_activated(); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->espList->closePersistentEditor(index); - } - else { - - displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->espList->closePersistentEditor(index); - } - } - } - } - catch (const std::exception &e) { - reportError(e.what()); - } -} - -bool MainWindow::populateMenuCategories(QMenu *menu, int targetID) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - const std::set &categories = modInfo->getCategories(); - - bool childEnabled = false; - - for (unsigned int i = 1; i < m_CategoryFactory.numCategories(); ++i) { - if (m_CategoryFactory.getParentID(i) == targetID) { - QMenu *targetMenu = menu; - if (m_CategoryFactory.hasChildren(i)) { - targetMenu = menu->addMenu(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); - } - - int id = m_CategoryFactory.getCategoryID(i); - QScopedPointer checkBox(new QCheckBox(targetMenu)); - bool enabled = categories.find(id) != categories.end(); - checkBox->setText(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); - if (enabled) { - childEnabled = true; - } - checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); - - QScopedPointer checkableAction(new QWidgetAction(targetMenu)); - checkableAction->setDefaultWidget(checkBox.take()); - checkableAction->setData(id); - targetMenu->addAction(checkableAction.take()); - - if (m_CategoryFactory.hasChildren(i)) { - if (populateMenuCategories(targetMenu, m_CategoryFactory.getCategoryID(i)) || enabled) { - targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); - } - } - } - } - return childEnabled; -} - -void MainWindow::replaceCategoriesFromMenu(QMenu *menu, int modRow) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow); - for (QAction* action : menu->actions()) { - if (action->menu() != nullptr) { - replaceCategoriesFromMenu(action->menu(), modRow); - } else { - QWidgetAction *widgetAction = qobject_cast(action); - if (widgetAction != nullptr) { - QCheckBox *checkbox = qobject_cast(widgetAction->defaultWidget()); - modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked()); - } - } - } -} - -void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow) -{ - if (referenceRow != -1 && referenceRow != modRow) { - ModInfo::Ptr editedModInfo = ModInfo::getByIndex(referenceRow); - for (QAction* action : menu->actions()) { - if (action->menu() != nullptr) { - addRemoveCategoriesFromMenu(action->menu(), modRow, referenceRow); - } else { - QWidgetAction *widgetAction = qobject_cast(action); - if (widgetAction != nullptr) { - QCheckBox *checkbox = qobject_cast(widgetAction->defaultWidget()); - int categoryId = widgetAction->data().toInt(); - bool checkedBefore = editedModInfo->categorySet(categoryId); - bool checkedAfter = checkbox->isChecked(); - - if (checkedBefore != checkedAfter) { // only update if the category was changed on the edited mod - ModInfo::Ptr currentModInfo = ModInfo::getByIndex(modRow); - currentModInfo->setCategory(categoryId, checkedAfter); - } - } - } - } - } else { - replaceCategoriesFromMenu(menu, modRow); - } -} - -void MainWindow::addRemoveCategories_MenuHandler() { - QMenu *menu = qobject_cast(sender()); - if (menu == nullptr) { - qCritical("not a menu?"); - return; - } - - QList selected; - for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { - selected.append(QPersistentModelIndex(idx)); - } - - if (selected.size() > 0) { - int minRow = INT_MAX; - int maxRow = -1; - - for (const QPersistentModelIndex &idx : selected) { - qDebug("change categories on: %s", qUtf8Printable(idx.data().toString())); - QModelIndex modIdx = mapToModel(m_OrganizerCore.modList(), idx); - if (modIdx.row() != m_ContextIdx.row()) { - addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row()); - } - if (idx.row() < minRow) minRow = idx.row(); - if (idx.row() > maxRow) maxRow = idx.row(); - } - replaceCategoriesFromMenu(menu, m_ContextIdx.row()); - - m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); - - for (const QPersistentModelIndex &idx : selected) { - ui->modList->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); - } - } else { - //For single mod selections, just do a replace - replaceCategoriesFromMenu(menu, m_ContextRow); - m_OrganizerCore.modList()->notifyChange(m_ContextRow); - } - - refreshFilters(); -} - -void MainWindow::replaceCategories_MenuHandler() { - QMenu *menu = qobject_cast(sender()); - if (menu == nullptr) { - qCritical("not a menu?"); - return; - } - - QList selected; - for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { - selected.append(QPersistentModelIndex(idx)); - } - - if (selected.size() > 0) { - QStringList selectedMods; - int minRow = INT_MAX; - int maxRow = -1; - for (int i = 0; i < selected.size(); ++i) { - QModelIndex temp = mapToModel(m_OrganizerCore.modList(), selected.at(i)); - selectedMods.append(temp.data().toString()); - replaceCategoriesFromMenu(menu, mapToModel(m_OrganizerCore.modList(), selected.at(i)).row()); - if (temp.row() < minRow) minRow = temp.row(); - if (temp.row() > maxRow) maxRow = temp.row(); - } - - m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); - - // find mods by their name because indices are invalidated - QAbstractItemModel *model = ui->modList->model(); - for (const QString &mod : selectedMods) { - QModelIndexList matches = model->match(model->index(0, 0), Qt::DisplayRole, mod, 1, - Qt::MatchFixedString | Qt::MatchCaseSensitive | Qt::MatchRecursive); - if (matches.size() > 0) { - ui->modList->selectionModel()->select(matches.at(0), QItemSelectionModel::Select | QItemSelectionModel::Rows); - } - } - } else { - //For single mod selections, just do a replace - replaceCategoriesFromMenu(menu, m_ContextRow); - m_OrganizerCore.modList()->notifyChange(m_ContextRow); - } - - refreshFilters(); -} - -void MainWindow::saveArchiveList() -{ - if (m_OrganizerCore.isArchivesInit()) { - SafeWriteFile archiveFile(m_OrganizerCore.currentProfile()->getArchivesFileName()); - for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { - QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i); - 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")); - } - } - } - if (archiveFile.commitIfDifferent(m_ArchiveListHash)) { - qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(m_OrganizerCore.currentProfile()->getArchivesFileName()))); - } - } else { - qWarning("archive list not initialised"); - } -} - -void MainWindow::checkModsForUpdates() -{ - statusBar()->show(); - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { - m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); - m_RefreshProgress->setRange(0, m_ModsToUpdate); - } else { - QString username, password; - if (m_OrganizerCore.settings().getNexusLogin(username, password)) { - m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); - } else { // otherwise there will be no endorsement info - MessageDialog::showMessage(tr("Not logged in, endorsement information will be wrong"), - this, true); - m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); - } - } -} - -void MainWindow::changeVersioningScheme() { - if (QMessageBox::question(this, tr("Continue?"), - tr("The versioning scheme decides which version is considered newer than another.\n" - "This function will guess the versioning scheme under the assumption that the installed version is outdated."), - QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) { - - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - - bool success = false; - - static VersionInfo::VersionScheme schemes[] = { VersionInfo::SCHEME_REGULAR, VersionInfo::SCHEME_DECIMALMARK, VersionInfo::SCHEME_NUMBERSANDLETTERS }; - - for (int i = 0; i < sizeof(schemes) / sizeof(VersionInfo::VersionScheme) && !success; ++i) { - VersionInfo verOld(info->getVersion().canonicalString(), schemes[i]); - VersionInfo verNew(info->getNewestVersion().canonicalString(), schemes[i]); - if (verOld < verNew) { - info->setVersion(verOld); - info->setNewestVersion(verNew); - success = true; - } - } - if (!success) { - QMessageBox::information(this, tr("Sorry"), - tr("I don't know a versioning scheme where %1 is newer than %2.").arg(info->getNewestVersion().canonicalString()).arg(info->getVersion().canonicalString()), - QMessageBox::Ok); - } - } -} - -void MainWindow::ignoreUpdate() { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - info->ignoreUpdate(true); - } - } - else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->ignoreUpdate(true); - } - if (m_ModListSortProxy != nullptr) { - m_ModListSortProxy->invalidate(); - } -} - -void MainWindow::unignoreUpdate() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - info->ignoreUpdate(false); - } - } - else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->ignoreUpdate(false); - } - if (m_ModListSortProxy != nullptr) { - m_ModListSortProxy->invalidate(); - } -} - -void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, - ModInfo::Ptr info) { - const std::set &categories = info->getCategories(); - for (int categoryID : categories) { - int catIdx = m_CategoryFactory.getCategoryIndex(categoryID); - QWidgetAction *action = new QWidgetAction(primaryCategoryMenu); - try { - QRadioButton *categoryBox = new QRadioButton( - m_CategoryFactory.getCategoryName(catIdx).replace('&', "&&"), - primaryCategoryMenu); - connect(categoryBox, &QRadioButton::toggled, [info, categoryID](bool enable) { - if (enable) { - info->setPrimaryCategory(categoryID); - } - }); - categoryBox->setChecked(categoryID == info->getPrimaryCategory()); - action->setDefaultWidget(categoryBox); - } catch (const std::exception &e) { - qCritical("failed to create category checkbox: %s", e.what()); - } - - action->setData(categoryID); - primaryCategoryMenu->addAction(action); - } -} - -void MainWindow::addPrimaryCategoryCandidates() -{ - QMenu *menu = qobject_cast(sender()); - if (menu == nullptr) { - qCritical("not a menu?"); - return; - } - menu->clear(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - - addPrimaryCategoryCandidates(menu, modInfo); -} - -void MainWindow::enableVisibleMods() -{ - if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all visible mods?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - m_ModListSortProxy->enableAllVisible(); - } -} - -void MainWindow::disableVisibleMods() -{ - if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all visible mods?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - m_ModListSortProxy->disableAllVisible(); - } -} - -void MainWindow::openInstanceFolder() -{ - QString dataPath = qApp->property("dataPath").toString(); - ::ShellExecuteW(nullptr, L"explore", ToWString(dataPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - - //opens BaseDirectory instead - //::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getBaseDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void MainWindow::openLogsFolder() -{ - QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()); - ::ShellExecuteW(nullptr, L"explore", ToWString(logsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void MainWindow::openInstallFolder() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(qApp->applicationDirPath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void MainWindow::openPluginsFolder() -{ - QString pluginsPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()); - ::ShellExecuteW(nullptr, L"explore", ToWString(pluginsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - - -void MainWindow::openProfileFolder() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void MainWindow::openIniFolder() -{ - if (m_OrganizerCore.currentProfile()->localSettingsEnabled()) - { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } - else { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } -} - -void MainWindow::openDownloadsFolder() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getDownloadDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void MainWindow::openModsFolder() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getModDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void MainWindow::openGameFolder() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void MainWindow::openMyGamesFolder() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - - -void MainWindow::exportModListCSV() -{ - //SelectionDialog selection(tr("Choose what to export")); - - //selection.addChoice(tr("Everything"), tr("All installed mods are included in the list"), 0); - //selection.addChoice(tr("Active Mods"), tr("Only active (checked) mods from your current profile are included"), 1); - //selection.addChoice(tr("Visible"), tr("All mods visible in the mod list are included"), 2); - - QDialog selection(this); - QGridLayout *grid = new QGridLayout; - selection.setWindowTitle(tr("Export to csv")); - - QLabel *csvDescription = new QLabel(); - csvDescription->setText(tr("CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.\nYou can also use online editors and converters instead.")); - grid->addWidget(csvDescription); - - QGroupBox *groupBoxRows = new QGroupBox(tr("Select what mods you want export:")); - QRadioButton *all = new QRadioButton(tr("All installed mods")); - QRadioButton *active = new QRadioButton(tr("Only active (checked) mods from your current profile")); - QRadioButton *visible = new QRadioButton(tr("All currently visible mods in the mod list")); - - QVBoxLayout *vbox = new QVBoxLayout; - vbox->addWidget(all); - vbox->addWidget(active); - vbox->addWidget(visible); - vbox->addStretch(1); - groupBoxRows->setLayout(vbox); - - - - grid->addWidget(groupBoxRows); - - QButtonGroup *buttonGroupRows = new QButtonGroup(); - buttonGroupRows->addButton(all, 0); - buttonGroupRows->addButton(active, 1); - buttonGroupRows->addButton(visible, 2); - buttonGroupRows->button(0)->setChecked(true); - - - - QGroupBox *groupBoxColumns = new QGroupBox(tr("Choose what Columns to export:")); - groupBoxColumns->setFlat(true); - - QCheckBox *mod_Priority = new QCheckBox(tr("Mod_Priority")); - mod_Priority->setChecked(true); - QCheckBox *mod_Name = new QCheckBox(tr("Mod_Name")); - mod_Name->setChecked(true); - QCheckBox *mod_Note = new QCheckBox(tr("Notes_column")); - QCheckBox *mod_Status = new QCheckBox(tr("Mod_Status")); - QCheckBox *primary_Category = new QCheckBox(tr("Primary_Category")); - QCheckBox *nexus_ID = new QCheckBox(tr("Nexus_ID")); - QCheckBox *mod_Nexus_URL = new QCheckBox(tr("Mod_Nexus_URL")); - QCheckBox *mod_Version = new QCheckBox(tr("Mod_Version")); - QCheckBox *install_Date = new QCheckBox(tr("Install_Date")); - QCheckBox *download_File_Name = new QCheckBox(tr("Download_File_Name")); - - QVBoxLayout *vbox1 = new QVBoxLayout; - vbox1->addWidget(mod_Priority); - vbox1->addWidget(mod_Name); - vbox1->addWidget(mod_Status); - vbox1->addWidget(mod_Note); - vbox1->addWidget(primary_Category); - vbox1->addWidget(nexus_ID); - vbox1->addWidget(mod_Nexus_URL); - vbox1->addWidget(mod_Version); - vbox1->addWidget(install_Date); - vbox1->addWidget(download_File_Name); - groupBoxColumns->setLayout(vbox1); - - grid->addWidget(groupBoxColumns); - - QPushButton *ok = new QPushButton("Ok"); - QPushButton *cancel = new QPushButton("Cancel"); - QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - - connect(buttons, SIGNAL(accepted()), &selection, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), &selection, SLOT(reject())); - - grid->addWidget(buttons); - - selection.setLayout(grid); - - - if (selection.exec() == QDialog::Accepted) { - - unsigned int numMods = ModInfo::getNumMods(); - int selectedRowID = buttonGroupRows->checkedId(); - - try { - QBuffer buffer; - buffer.open(QIODevice::ReadWrite); - CSVBuilder builder(&buffer); - builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS); - std::vector > fields; - if (mod_Priority->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Priority"), CSVBuilder::TYPE_STRING)); - if (mod_Name->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Name"), CSVBuilder::TYPE_STRING)); - if (mod_Status->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Status"), CSVBuilder::TYPE_STRING)); - if (mod_Note->isChecked()) - fields.push_back(std::make_pair(QString("#Note"), CSVBuilder::TYPE_STRING)); - if (primary_Category->isChecked()) - fields.push_back(std::make_pair(QString("#Primary_Category"), CSVBuilder::TYPE_STRING)); - if (nexus_ID->isChecked()) - fields.push_back(std::make_pair(QString("#Nexus_ID"), CSVBuilder::TYPE_INTEGER)); - if (mod_Nexus_URL->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Nexus_URL"), CSVBuilder::TYPE_STRING)); - if (mod_Version->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Version"), CSVBuilder::TYPE_STRING)); - if (install_Date->isChecked()) - fields.push_back(std::make_pair(QString("#Install_Date"), CSVBuilder::TYPE_STRING)); - if (download_File_Name->isChecked()) - fields.push_back(std::make_pair(QString("#Download_File_Name"), CSVBuilder::TYPE_STRING)); - - builder.setFields(fields); - - builder.writeHeader(); - - for (unsigned int i = 0; i < numMods; ++i) { - ModInfo::Ptr info = ModInfo::getByIndex(i); - bool enabled = m_OrganizerCore.currentProfile()->modEnabled(i); - if ((selectedRowID == 1) && !enabled) { - continue; - } - else if ((selectedRowID == 2) && !m_ModListSortProxy->filterMatchesMod(info, enabled)) { - continue; - } - std::vector flags = info->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end()) && - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end())) { - if (mod_Priority->isChecked()) - builder.setRowField("#Mod_Priority", QString("%1").arg(m_OrganizerCore.currentProfile()->getModPriority(i), 4, 10, QChar('0'))); - if (mod_Name->isChecked()) - builder.setRowField("#Mod_Name", info->name()); - if (mod_Status->isChecked()) - builder.setRowField("#Mod_Status", (enabled)? "Enabled" : "Disabled"); - if (mod_Note->isChecked()) - builder.setRowField("#Note", QString("%1").arg(info->comments().remove(','))); - if (primary_Category->isChecked()) - builder.setRowField("#Primary_Category", (m_CategoryFactory.categoryExists(info->getPrimaryCategory())) ? m_CategoryFactory.getCategoryName(info->getPrimaryCategory()) : ""); - if (nexus_ID->isChecked()) - builder.setRowField("#Nexus_ID", info->getNexusID()); - if (mod_Nexus_URL->isChecked()) - builder.setRowField("#Mod_Nexus_URL",(info->getNexusID()>0)? NexusInterface::instance(&m_PluginContainer)->getModURL(info->getNexusID(), info->getGameName()) : ""); - if (mod_Version->isChecked()) - builder.setRowField("#Mod_Version", info->getVersion().canonicalString()); - if (install_Date->isChecked()) - builder.setRowField("#Install_Date", info->creationTime().toString("yyyy/MM/dd HH:mm:ss")); - if (download_File_Name->isChecked()) - builder.setRowField("#Download_File_Name", info->getInstallationFile()); - - builder.writeRow(); - } - } - - SaveTextAsDialog saveDialog(this); - saveDialog.setText(buffer.data()); - saveDialog.exec(); - } - catch (const std::exception &e) { - reportError(tr("export failed: %1").arg(e.what())); - } - } -} - -static void addMenuAsPushButton(QMenu *menu, QMenu *subMenu) -{ - QPushButton *pushBtn = new QPushButton(subMenu->title()); - pushBtn->setMenu(subMenu); - QWidgetAction *action = new QWidgetAction(menu); - action->setDefaultWidget(pushBtn); - menu->addAction(action); -} - -QMenu *MainWindow::openFolderMenu() -{ - - QMenu *FolderMenu = new QMenu(this); - - FolderMenu->addAction(tr("Open Game folder"), this, SLOT(openGameFolder())); - - FolderMenu->addAction(tr("Open MyGames folder"), this, SLOT(openMyGamesFolder())); - - FolderMenu->addAction(tr("Open INIs folder"), this, SLOT(openIniFolder())); - - FolderMenu->addSeparator(); - - FolderMenu->addAction(tr("Open Instance folder"), this, SLOT(openInstanceFolder())); - - FolderMenu->addAction(tr("Open Mods folder"), this, SLOT(openModsFolder())); - - FolderMenu->addAction(tr("Open Profile folder"), this, SLOT(openProfileFolder())); - - FolderMenu->addAction(tr("Open Downloads folder"), this, SLOT(openDownloadsFolder())); - - FolderMenu->addSeparator(); - - FolderMenu->addAction(tr("Open MO2 Install folder"), this, SLOT(openInstallFolder())); - - FolderMenu->addAction(tr("Open MO2 Plugins folder"), this, SLOT(openPluginsFolder())); - - FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder())); - - - return FolderMenu; -} - -void MainWindow::initModListContextMenu(QMenu *menu) -{ - menu->addAction(tr("Install Mod..."), this, SLOT(installMod_clicked())); - menu->addAction(tr("Create empty mod"), this, SLOT(createEmptyMod_clicked())); - menu->addAction(tr("Create Separator"), this, SLOT(createSeparator_clicked())); - - menu->addSeparator(); - - menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); - menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - menu->addAction(tr("Check all for update"), this, SLOT(checkModsForUpdates())); - menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh())); - menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); -} - -void MainWindow::addModSendToContextMenu(QMenu *menu) -{ - if (m_ModListSortProxy->sortColumn() != ModList::COL_PRIORITY) - return; - - QMenu *sub_menu = new QMenu(menu); - sub_menu->setTitle(tr("Send to")); - sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedModsToTop_clicked())); - sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedModsToBottom_clicked())); - sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedModsToPriority_clicked())); - sub_menu->addAction(tr("Separator..."), this, SLOT(sendSelectedModsToSeparator_clicked())); - - menu->addMenu(sub_menu); - menu->addSeparator(); -} - -void MainWindow::addPluginSendToContextMenu(QMenu *menu) -{ - if (m_PluginListSortProxy->sortColumn() != PluginList::COL_PRIORITY) - return; - - QMenu *sub_menu = new QMenu(this); - sub_menu->setTitle(tr("Send to")); - sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedPluginsToTop_clicked())); - sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedPluginsToBottom_clicked())); - sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedPluginsToPriority_clicked())); - - menu->addMenu(sub_menu); - menu->addSeparator(); -} - -void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) -{ - try { - QTreeView *modList = findChild("modList"); - - m_ContextIdx = mapToModel(m_OrganizerCore.modList(), modList->indexAt(pos)); - m_ContextRow = m_ContextIdx.row(); - - if (m_ContextRow == -1) { - // no selection - QMenu menu(this); - initModListContextMenu(&menu); - menu.exec(modList->mapToGlobal(pos)); - } else { - QMenu menu(this); - - QMenu *allMods = new QMenu(&menu); - initModListContextMenu(allMods); - allMods->setTitle(tr("All Mods")); - menu.addMenu(allMods); - menu.addSeparator(); - - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - std::vector flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { - if (QDir(info->absolutePath()).count() > 2) { - menu.addAction(tr("Sync to Mods..."), &m_OrganizerCore, SLOT(syncOverwrite())); - menu.addAction(tr("Create Mod..."), this, SLOT(createModFromOverwrite())); - menu.addAction(tr("Move content to Mod..."), this, SLOT(moveOverwriteContentToExistingMod())); - menu.addAction(tr("Clear Overwrite..."), this, SLOT(clearOverwrite())); - } - menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); - } 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_SEPARATOR) != flags.end()){ - menu.addSeparator(); - QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu); - populateMenuCategories(addRemoveCategoriesMenu, 0); - connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler())); - addMenuAsPushButton(&menu, addRemoveCategoriesMenu); - QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu); - connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); - addMenuAsPushButton(&menu, primaryCategoryMenu); - menu.addSeparator(); - menu.addAction(tr("Rename Separator..."), this, SLOT(renameMod_clicked())); - menu.addAction(tr("Remove Separator..."), this, SLOT(removeMod_clicked())); - menu.addSeparator(); - addModSendToContextMenu(&menu); - menu.addAction(tr("Select Color..."), this, SLOT(setColor_clicked())); - if(info->getColor().isValid()) - menu.addAction(tr("Reset Color"), this, SLOT(resetColor_clicked())); - menu.addSeparator(); - } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) { - addModSendToContextMenu(&menu); - } else { - QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu); - populateMenuCategories(addRemoveCategoriesMenu, 0); - connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler())); - addMenuAsPushButton(&menu, addRemoveCategoriesMenu); - - QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu); - connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); - addMenuAsPushButton(&menu, primaryCategoryMenu); - - menu.addSeparator(); - - if (info->downgradeAvailable()) { - menu.addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme())); - } - - if (info->updateIgnored()) { - menu.addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate())); - } - else { - if (info->updateAvailable() || info->downgradeAvailable()) { - menu.addAction(tr("Ignore update"), this, SLOT(ignoreUpdate())); - } - } - menu.addSeparator(); - - menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedMods_clicked())); - menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedMods_clicked())); - - menu.addSeparator(); - - addModSendToContextMenu(&menu); - - menu.addAction(tr("Rename Mod..."), this, SLOT(renameMod_clicked())); - menu.addAction(tr("Reinstall Mod"), this, SLOT(reinstallMod_clicked())); - menu.addAction(tr("Remove Mod..."), this, SLOT(removeMod_clicked())); - menu.addAction(tr("Create Backup"), this, SLOT(backupMod_clicked())); - - menu.addSeparator(); - - if (info->getNexusID() > 0 && Settings::instance().endorsementIntegration()) { - switch (info->endorsedState()) { - case ModInfo::ENDORSED_TRUE: { - menu.addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked())); - } break; - case ModInfo::ENDORSED_FALSE: { - menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); - menu.addAction(tr("Won't endorse"), this, SLOT(dontendorse_clicked())); - } break; - case ModInfo::ENDORSED_NEVER: { - menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); - } break; - default: { - QAction *action = new QAction(tr("Endorsement state unknown"), &menu); - action->setEnabled(false); - menu.addAction(action); - } break; - } - } - - menu.addSeparator(); - - std::vector flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) { - menu.addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked())); - } - - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) { - menu.addAction(tr("Mark as converted/working"), this, SLOT(markConverted_clicked())); - } - - if (info->getNexusID() > 0) { - menu.addAction(tr("Visit on Nexus"), this, SLOT(visitOnNexus_clicked())); - } else if ((info->getURL() != "")) { - menu.addAction(tr("Visit web page"), this, SLOT(visitWebPage_clicked())); - } - - menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); - } - - 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)); - } - } catch (const std::exception &e) { - reportError(tr("Exception: ").arg(e.what())); - } catch (...) { - reportError(tr("Unknown exception")); - } -} - - -void MainWindow::on_categoriesList_itemSelectionChanged() -{ - QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows(); - std::vector categories; - std::vector content; - for (const QModelIndex &index : indices) { - int filterType = index.data(Qt::UserRole + 1).toInt(); - if ((filterType == ModListSortProxy::TYPE_CATEGORY) - || (filterType == ModListSortProxy::TYPE_SPECIAL)) { - int categoryId = index.data(Qt::UserRole).toInt(); - if (categoryId != CategoryFactory::CATEGORY_NONE) { - categories.push_back(categoryId); - } - } else if (filterType == ModListSortProxy::TYPE_CONTENT) { - int contentId = index.data(Qt::UserRole).toInt(); - content.push_back(contentId); - } - } - - m_ModListSortProxy->setCategoryFilter(categories); - m_ModListSortProxy->setContentFilter(content); - ui->clickBlankButton->setEnabled(categories.size() > 0 || content.size() >0); - - if (indices.count() == 0) { - ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); - } else if (indices.count() > 1) { - ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); - } else { - ui->currentCategoryLabel->setText(QString("(%1)").arg(indices.first().data().toString())); - } - ui->modList->reset(); -} - - -void MainWindow::deleteSavegame_clicked() -{ - SaveGameInfo const *info = m_OrganizerCore.managedGame()->feature(); - - QString savesMsgLabel; - QStringList deleteFiles; - - int count = 0; - - for (const QModelIndex &idx : ui->savegameList->selectionModel()->selectedIndexes()) { - QString name = idx.data(Qt::UserRole).toString(); - - if (count < 10) { - savesMsgLabel += "
  • " + QFileInfo(name).completeBaseName() + "
  • "; - } - ++count; - - if (info == nullptr) { - deleteFiles.push_back(name); - } else { - ISaveGame const *save = info->getSaveGameInfo(name); - deleteFiles += save->allFiles(); - delete save; - } - } - - if (count > 10) { - savesMsgLabel += "
  • ... " + tr("%1 more").arg(count - 10) + "
  • "; - } - - if (QMessageBox::question(this, tr("Confirm"), - tr("Are you sure you want to remove the following %n save(s)?
    " - "
      %1

    " - "Removed saves will be sent to the Recycle Bin.", "", count) - .arg(savesMsgLabel), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - shellDelete(deleteFiles, true); // recycle bin delete. - refreshSaveList(); - } -} - - -void MainWindow::fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets) -{ - ActivateModsDialog dialog(missingAssets, this); - if (dialog.exec() == QDialog::Accepted) { - // activate the required mods, then enable all esps - std::set modsToActivate = dialog.getModsToActivate(); - for (std::set::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) { - if ((*iter != "") && (*iter != "")) { - unsigned int modIndex = ModInfo::getIndex(*iter); - m_OrganizerCore.currentProfile()->setModEnabled(modIndex, true); - } - } - - m_OrganizerCore.currentProfile()->writeModlist(); - m_OrganizerCore.refreshLists(); - - std::set espsToActivate = dialog.getESPsToActivate(); - for (std::set::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) { - m_OrganizerCore.pluginList()->enableESP(*iter); - } - m_OrganizerCore.saveCurrentLists(); - } -} - - -void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos) -{ - QItemSelectionModel *selection = ui->savegameList->selectionModel(); - - if (!selection->hasSelection()) { - return; - } - - QMenu menu; - QAction *action = menu.addAction(tr("Enable Mods...")); - action->setEnabled(false); - - if (selection->selectedIndexes().count() == 1) { - SaveGameInfo const *info = this->m_OrganizerCore.managedGame()->feature(); - if (info != nullptr) { - QString save = ui->savegameList->currentItem()->data(Qt::UserRole).toString(); - SaveGameInfo::MissingAssets missing = info->getMissingAssets(save); - if (missing.size() != 0) { - connect(action, &QAction::triggered, this, [this, missing]{ fixMods_clicked(missing); }); - action->setEnabled(true); - } - } - } - - QString deleteMenuLabel = tr("Delete %n save(s)", "", selection->selectedIndexes().count()); - - menu.addAction(deleteMenuLabel, this, SLOT(deleteSavegame_clicked())); - - menu.exec(ui->savegameList->mapToGlobal(pos)); -} - -void MainWindow::linkToolbar() -{ - Executable &exe(getSelectedExecutable()); - exe.showOnToolbar(!exe.isShownOnToolbar()); - ui->linkButton->menu()->actions().at(static_cast(ShortcutType::Toolbar))->setIcon(exe.isShownOnToolbar() ? QIcon(":/MO/gui/remove") : QIcon(":/MO/gui/link")); - updateToolBar(); -} - -namespace { -QString getLinkfile(const QString &dir, const Executable &exec) -{ - return QDir::fromNativeSeparators(dir) + "/" + exec.m_Title + ".lnk"; -} - -QString getDesktopLinkfile(const Executable &exec) -{ - return getLinkfile(getDesktopDirectory(), exec); -} - -QString getStartMenuLinkfile(const Executable &exec) -{ - return getLinkfile(getStartMenuDirectory(), exec); -} -} - -void MainWindow::addWindowsLink(const ShortcutType mapping) -{ - const Executable &selectedExecutable(getSelectedExecutable()); - QString const linkName = getLinkfile(mapping == ShortcutType::Desktop ? getDesktopDirectory() : getStartMenuDirectory(), - selectedExecutable); - - if (QFile::exists(linkName)) { - if (QFile::remove(linkName)) { - ui->linkButton->menu()->actions().at(static_cast(mapping))->setIcon(QIcon(":/MO/gui/link")); - } else { - reportError(tr("failed to remove %1").arg(linkName)); - } - } else { - QFileInfo const exeInfo(qApp->applicationFilePath()); - // create link - QString executable = QDir::toNativeSeparators(selectedExecutable.m_BinaryInfo.absoluteFilePath()); - - std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); - std::wstring parameter = ToWString( - QString("\"moshortcut://%1:%2\"").arg(InstanceManager::instance().currentInstance(),selectedExecutable.m_Title)); - std::wstring description = ToWString(QString("Run %1 with ModOrganizer").arg(selectedExecutable.m_Title)); - std::wstring iconFile = ToWString(executable); - std::wstring currentDirectory = ToWString(QDir::toNativeSeparators(qApp->applicationDirPath())); - - if (CreateShortcut(targetFile.c_str() - , parameter.c_str() - , QDir::toNativeSeparators(linkName).toUtf8().constData() - , description.c_str() - , (selectedExecutable.usesOwnIcon() ? iconFile.c_str() : nullptr), 0 - , currentDirectory.c_str()) == 0) { - ui->linkButton->menu()->actions().at(static_cast(mapping))->setIcon(QIcon(":/MO/gui/remove")); - } else { - reportError(tr("failed to create %1").arg(linkName)); - } - } -} - -void MainWindow::linkDesktop() -{ - addWindowsLink(ShortcutType::Desktop); -} - -void MainWindow::linkMenu() -{ - addWindowsLink(ShortcutType::StartMenu); -} - -void MainWindow::on_actionSettings_triggered() -{ - Settings &settings = m_OrganizerCore.settings(); - - QString oldModDirectory(settings.getModDirectory()); - QString oldCacheDirectory(settings.getCacheDirectory()); - QString oldProfilesDirectory(settings.getProfileDirectory()); - QString oldManagedGameDirectory(settings.getManagedGameDirectory()); - bool oldDisplayForeign(settings.displayForeign()); - bool proxy = settings.useProxy(); - DownloadManager *dlManager = m_OrganizerCore.downloadManager(); - - settings.query(&m_PluginContainer, this); - - if (oldManagedGameDirectory != settings.getManagedGameDirectory()) { - QMessageBox::about(this, tr("Restarting MO"), - tr("Changing the managed game directory requires restarting MO.\n" - "Any pending downloads will be paused.\n\n" - "Click OK to restart MO now.")); - dlManager->pauseAll(); - qApp->exit(INT_MAX); - } - - InstallationManager *instManager = m_OrganizerCore.installationManager(); - instManager->setModsDirectory(settings.getModDirectory()); - instManager->setDownloadDirectory(settings.getDownloadDirectory()); - - fixCategories(); - refreshFilters(); - - if (settings.getProfileDirectory() != oldProfilesDirectory) { - refreshProfiles(); - } - - if (dlManager->getOutputDirectory() != settings.getDownloadDirectory()) { - if (dlManager->downloadsInProgress()) { - MessageDialog::showMessage(tr("Can't change download directory while " - "downloads are in progress!"), - this); - } else { - dlManager->setOutputDirectory(settings.getDownloadDirectory()); - } - } - dlManager->setPreferredServers(settings.getPreferredServers()); - - if ((settings.getModDirectory() != oldModDirectory) - || (settings.displayForeign() != oldDisplayForeign)) { - m_OrganizerCore.profileRefresh(); - } - - const auto state = settings.archiveParsing(); - if (state != m_OrganizerCore.getArchiveParsing()) - { - m_OrganizerCore.setArchiveParsing(state); - if (!state) - { - ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked); - ui->showArchiveDataCheckBox->setEnabled(false); - m_showArchiveData = false; - } - else - { - ui->showArchiveDataCheckBox->setCheckState(Qt::Checked); - ui->showArchiveDataCheckBox->setEnabled(true); - m_showArchiveData = true; - } - m_OrganizerCore.refreshModList(); - m_OrganizerCore.refreshDirectoryStructure(); - m_OrganizerCore.refreshLists(); - } - - if (settings.getCacheDirectory() != oldCacheDirectory) { - NexusInterface::instance(&m_PluginContainer)->setCacheDirectory(settings.getCacheDirectory()); - } - - if (proxy != settings.useProxy()) { - activateProxy(settings.useProxy()); - } - - NexusInterface::instance(&m_PluginContainer)->setNMMVersion(settings.getNMMVersion()); - - updateDownloadView(); - - m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist()); - m_OrganizerCore.cycleDiagnostics(); -} - - -void MainWindow::on_actionNexus_triggered() -{ - const IPluginGame *game = m_OrganizerCore.managedGame(); - QString gameName = game->gameShortName(); - if (game->gameNexusName().isEmpty() && game->primarySources().count()) - gameName = game->primarySources()[0]; - QDesktopServices::openUrl(QUrl(NexusInterface::instance(&m_PluginContainer)->getGameURL(gameName))); -} - - -void MainWindow::linkClicked(const QString &url) -{ - QDesktopServices::openUrl(QUrl(url)); -} - - -void MainWindow::installTranslator(const QString &name) -{ - QTranslator *translator = new QTranslator(this); - QString fileName = name + "_" + m_CurrentLanguage; - if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) { - if (m_CurrentLanguage.compare("en", Qt::CaseInsensitive)) { - qDebug("localization file %s not found", qUtf8Printable(fileName)); - } // we don't actually expect localization files for English - } - - qApp->installTranslator(translator); - m_Translators.push_back(translator); -} - - -void MainWindow::languageChange(const QString &newLanguage) -{ - for (QTranslator *trans : m_Translators) { - qApp->removeTranslator(trans); - } - m_Translators.clear(); - - m_CurrentLanguage = newLanguage; - - installTranslator("qt"); - installTranslator("qtbase"); - installTranslator(ToQString(AppConfig::translationPrefix())); - for (const QString &fileName : m_PluginContainer.pluginFileNames()) { - installTranslator(QFileInfo(fileName).baseName()); - } - ui->retranslateUi(this); - qDebug("loaded language %s", qUtf8Printable(newLanguage)); - - ui->profileBox->setItemText(0, QObject::tr("")); - - createHelpWidget(); - - updateDownloadView(); - updateProblemsButton(); - - QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn); - initModListContextMenu(listOptionsMenu); - ui->listOptionsBtn->setMenu(listOptionsMenu); - - ui->openFolderMenu->setMenu(openFolderMenu()); -} - -void MainWindow::writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry) -{ - for (FileEntry::Ptr current : directoryEntry.getFiles()) { - bool isArchive = false; - int origin = current->getOrigin(isArchive); - if (isArchive) { - // TODO: don't list files from archives. maybe make this an option? - continue; - } - QString fullName = directory + "\\" + ToQString(current->getName()); - file.write(fullName.toUtf8()); - - file.write("\t("); - file.write(ToQString(m_OrganizerCore.directoryStructure()->getOriginByID(origin).getName()).toUtf8()); - file.write(")\r\n"); - } - - // recurse into subdirectories - std::vector::const_iterator current, end; - directoryEntry.getSubDirectories(current, end); - for (; current != end; ++current) { - writeDataToFile(file, directory + "\\" + ToQString((*current)->getName()), **current); - } -} - -void MainWindow::writeDataToFile() -{ - QString fileName = QFileDialog::getSaveFileName(this); - if (!fileName.isEmpty()) { - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly)) { - reportError(tr("failed to write to file %1").arg(fileName)); - } - - writeDataToFile(file, "data", *m_OrganizerCore.directoryStructure()); - file.close(); - - MessageDialog::showMessage(tr("%1 written").arg(QDir::toNativeSeparators(fileName)), this); - } -} - - -int MainWindow::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) -{ - QString extension = targetInfo.suffix(); - if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || - (extension.compare("com", Qt::CaseInsensitive) == 0) || - (extension.compare("bat", Qt::CaseInsensitive) == 0)) { - binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); - arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - return 1; - } else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { - binaryInfo = targetInfo; - return 1; - } else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { - // types that need to be injected into - std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); - QString binaryPath; - - { // try to find java automatically - WCHAR buffer[MAX_PATH]; - if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(buffer, &binaryType)) { - qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); - } else if (binaryType == SCS_32BIT_BINARY) { - binaryPath = ToQString(buffer); - } - } - } - if (binaryPath.isEmpty() && (extension == "jar")) { - // second attempt: look to the registry - QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (javaReg.contains("CurrentVersion")) { - QString currentVersion = javaReg.value("CurrentVersion").toString(); - binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); - } - } - if (binaryPath.isEmpty()) { - binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); - } - if (binaryPath.isEmpty()) { - return 0; - } - binaryInfo = QFileInfo(binaryPath); - if (extension == "jar") { - arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } else { - arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - return 1; - } else { - return 2; - } -} - - -void MainWindow::addAsExecutable() -{ - if (m_ContextItem != nullptr) { - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - QString name = QInputDialog::getText(this, tr("Enter Name"), - tr("Please enter a name for the executable"), QLineEdit::Normal, - targetInfo.baseName()); - if (!name.isEmpty()) { - //Note: If this already exists, you'll lose custom settings - m_OrganizerCore.executablesList()->addExecutable(name, - binaryInfo.absoluteFilePath(), - arguments, - targetInfo.absolutePath(), - QString(), - Executable::CustomExecutable); - refreshExecutablesList(); - } - } break; - case 2: { - QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable.")); - } break; - default: { - // nop - } break; - } - } -} - - -void MainWindow::originModified(int originID) -{ - FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID); - origin.enable(false); - m_OrganizerCore.directoryStructure()->addFromOrigin(origin.getName(), origin.getPath(), origin.getPriority()); - DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure()); -} - - -void MainWindow::hideFile() -{ - QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); - QString newName = oldName + ModInfo::s_HiddenExt; - - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return; - } - } else { - return; - } - } - - if (QFile::rename(oldName, newName)) { - originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); - refreshDataTreeKeepExpandedNodes(); - } else { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(oldName).arg(QDir::toNativeSeparators(newName))); - } -} - - -void MainWindow::unhideFile() -{ - QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); - QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return; - } - } else { - return; - } - } - if (QFile::rename(oldName, newName)) { - originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); - refreshDataTreeKeepExpandedNodes(); - } else { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); - } -} - - -void MainWindow::enableSelectedPlugins_clicked() -{ - m_OrganizerCore.pluginList()->enableSelected(ui->espList->selectionModel()); -} - - -void MainWindow::disableSelectedPlugins_clicked() -{ - m_OrganizerCore.pluginList()->disableSelected(ui->espList->selectionModel()); -} - -void MainWindow::sendSelectedPluginsToTop_clicked() -{ - m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), 0); -} - -void MainWindow::sendSelectedPluginsToBottom_clicked() -{ - m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), INT_MAX); -} - -void MainWindow::sendSelectedPluginsToPriority_clicked() -{ - bool ok; - int newPriority = QInputDialog::getInt(this, - tr("Set Priority"), tr("Set the priority of the selected plugins"), - 0, 0, INT_MAX, 1, &ok); - if (!ok) return; - - m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), newPriority); -} - - -void MainWindow::enableSelectedMods_clicked() -{ - m_OrganizerCore.modList()->enableSelected(ui->modList->selectionModel()); - if (m_ModListSortProxy != nullptr) { - m_ModListSortProxy->invalidate(); - } -} - - -void MainWindow::disableSelectedMods_clicked() -{ - m_OrganizerCore.modList()->disableSelected(ui->modList->selectionModel()); - if (m_ModListSortProxy != nullptr) { - m_ModListSortProxy->invalidate(); - } -} - - -void MainWindow::previewDataFile() -{ - QString fileName = QDir::fromNativeSeparators(m_ContextItem->data(0, Qt::UserRole).toString()); - - // what we have is an absolute path to the file in its actual location (for the primary origin) - // what we want is the path relative to the virtual data directory - - // we need to look in the virtual directory for the file to make sure the info is up to date. - - // check if the file comes from the actual data folder instead of a mod - QDir gameDirectory = m_OrganizerCore.managedGame()->dataDirectory().absolutePath(); - QString relativePath = gameDirectory.relativeFilePath(fileName); - QDir dirRelativePath = gameDirectory.relativeFilePath(fileName); - // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case - if (!dirRelativePath.isAbsolute() && !relativePath.startsWith("..")) { - fileName = relativePath; - } - else { - // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory - int offset = m_OrganizerCore.settings().getModDirectory().size() + 1; - offset = fileName.indexOf("/", offset); - fileName = fileName.mid(offset + 1); - } - - - - const FileEntry::Ptr file = m_OrganizerCore.directoryStructure()->searchFile(ToWString(fileName), nullptr); - - if (file.get() == nullptr) { - reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); - return; - } - - // set up preview dialog - PreviewDialog preview(fileName); - auto addFunc = [&] (int originId) { - FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originId); - QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; - if (QFile::exists(filePath)) { - // it's very possible the file doesn't exist, because it's inside an archive. we don't support that - QWidget *wid = m_PluginContainer.previewGenerator().genPreview(filePath); - if (wid == nullptr) { - reportError(tr("failed to generate preview for %1").arg(filePath)); - } else { - preview.addVariant(ToQString(origin.getName()), wid); - } - } - }; - - addFunc(file->getOrigin()); - for (auto alt : file->getAlternatives()) { - addFunc(alt.first); - } - if (preview.numVariants() > 0) { - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(preview.objectName()); - if (settings.contains(key)) { - preview.restoreGeometry(settings.value(key).toByteArray()); - } - preview.exec(); - settings.setValue(key, preview.saveGeometry()); - } else { - QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); - } -} - -void MainWindow::openDataFile() -{ - if (m_ContextItem != nullptr) { - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - m_OrganizerCore.spawnBinaryDirect( - binaryInfo, arguments, m_OrganizerCore.currentProfile()->name(), - targetInfo.absolutePath(), "", ""); - } break; - case 2: { - ::ShellExecuteW(nullptr, L"open", - ToWString(targetInfo.absoluteFilePath()).c_str(), - nullptr, nullptr, SW_SHOWNORMAL); - } break; - default: { - // nop - } break; - } - } -} - - -void MainWindow::updateAvailable() -{ - for (QAction *action : ui->toolBar->actions()) { - if (action->text() == tr("Update")) { - action->setEnabled(true); - action->setToolTip(tr("Update available")); - break; - } - } -} - - -void MainWindow::motdReceived(const QString &motd) -{ - // don't show motd after 5 seconds, may be annoying. Hopefully the user's - // internet connection is faster next time - if (m_StartTime.secsTo(QTime::currentTime()) < 5) { - uint hash = qHash(motd); - if (hash != m_OrganizerCore.settings().getMotDHash()) { - MotDDialog dialog(motd); - dialog.exec(); - m_OrganizerCore.settings().setMotDHash(hash); - } - } - - ui->actionEndorseMO->setVisible(false); -} - - -void MainWindow::notEndorsedYet() -{ - if (!Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { - ui->actionEndorseMO->setVisible(true); - } -} - - -void MainWindow::wontEndorse() -{ - Settings::instance().directInterface().setValue("wont_endorse_MO", true); - ui->actionEndorseMO->setVisible(false); -} - - -void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) -{ - QTreeWidget *dataTree = findChild("dataTree"); - m_ContextItem = dataTree->itemAt(pos.x(), pos.y()); - - QMenu menu; - if ((m_ContextItem != nullptr) && (m_ContextItem->childCount() == 0) - && (m_ContextItem->data(0, Qt::UserRole + 3).toBool() != true)) { - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable())); - - QString fileName = m_ContextItem->text(0); - if (m_PluginContainer.previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { - menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); - } - - // offer to hide/unhide file, but not for files from archives - if (!m_ContextItem->data(0, Qt::UserRole + 1).toBool()) { - if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile())); - } else { - menu.addAction(tr("Hide"), this, SLOT(hideFile())); - } - } - - menu.addSeparator(); - } - menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile())); - menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked())); - - menu.exec(dataTree->mapToGlobal(pos)); -} - -void MainWindow::on_conflictsCheckBox_toggled(bool) -{ - refreshDataTreeKeepExpandedNodes(); -} - - -void MainWindow::on_actionUpdate_triggered() -{ - m_OrganizerCore.startMOUpdate(); -} - - -void MainWindow::on_actionEndorseMO_triggered() -{ - // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now - IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); - if (!game) return; - - if (QMessageBox::question(this, tr("Endorse Mod Organizer"), - tr("Do you want to endorse Mod Organizer on %1 now?").arg( - NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement( - game->gameShortName(), game->nexusModOrganizerID(), true, this, QVariant(), QString()); - } -} - - -void MainWindow::initDownloadView() -{ - DownloadList *sourceModel = new DownloadList(m_OrganizerCore.downloadManager(), ui->downloadView); - DownloadListSortProxy *sortProxy = new DownloadListSortProxy(m_OrganizerCore.downloadManager(), ui->downloadView); - sortProxy->setSourceModel(sourceModel); - connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), sortProxy, SLOT(updateFilter(QString))); - connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(downloadFilterChanged(QString))); - - ui->downloadView->setSourceModel(sourceModel); - ui->downloadView->setModel(sortProxy); - ui->downloadView->setManager(m_OrganizerCore.downloadManager()); - ui->downloadView->setItemDelegate(new DownloadProgressDelegate(m_OrganizerCore.downloadManager(), sortProxy, ui->downloadView)); - updateDownloadView(); - - connect(ui->downloadView, SIGNAL(installDownload(int)), &m_OrganizerCore, SLOT(installDownload(int))); - connect(ui->downloadView, SIGNAL(queryInfo(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfo(int))); - connect(ui->downloadView, SIGNAL(visitOnNexus(int)), m_OrganizerCore.downloadManager(), SLOT(visitOnNexus(int))); - connect(ui->downloadView, SIGNAL(openFile(int)), m_OrganizerCore.downloadManager(), SLOT(openFile(int))); - connect(ui->downloadView, SIGNAL(openInDownloadsFolder(int)), m_OrganizerCore.downloadManager(), SLOT(openInDownloadsFolder(int))); - connect(ui->downloadView, SIGNAL(removeDownload(int, bool)), m_OrganizerCore.downloadManager(), SLOT(removeDownload(int, bool))); - connect(ui->downloadView, SIGNAL(restoreDownload(int)), m_OrganizerCore.downloadManager(), SLOT(restoreDownload(int))); - connect(ui->downloadView, SIGNAL(cancelDownload(int)), m_OrganizerCore.downloadManager(), SLOT(cancelDownload(int))); - connect(ui->downloadView, SIGNAL(pauseDownload(int)), m_OrganizerCore.downloadManager(), SLOT(pauseDownload(int))); - connect(ui->downloadView, SIGNAL(resumeDownload(int)), this, SLOT(resumeDownload(int))); -} - -void MainWindow::updateDownloadView() -{ - // set the view attribute and default row sizes - if (m_OrganizerCore.settings().compactDownloads()) { - ui->downloadView->setProperty("downloadView", "compact"); - setStyleSheet("DownloadListWidget::item { padding: 4px 2px; }"); - } else { - ui->downloadView->setProperty("downloadView", "standard"); - setStyleSheet("DownloadListWidget::item { padding: 16px 4px; }"); - } - //setStyleSheet("DownloadListWidget::item:hover { padding: 0px; }"); - //setStyleSheet("DownloadListWidget::item:selected { padding: 0px; }"); - - // reapply global stylesheet on the widget level (!) to override the defaults - //ui->downloadView->setStyleSheet(styleSheet()); - - ui->downloadView->setMetaDisplay(m_OrganizerCore.settings().metaDownloads()); - ui->downloadView->style()->unpolish(ui->downloadView); - ui->downloadView->style()->polish(ui->downloadView); - qobject_cast(ui->downloadView->header())->customResizeSections(); - m_OrganizerCore.downloadManager()->refreshList(); -} - -void MainWindow::modDetailsUpdated(bool) -{ - if (--m_ModsToUpdate == 0) { - statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); - break; - } - } - m_RefreshProgress->setVisible(false); - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } -} - -void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int) -{ - m_ModsToUpdate -= static_cast(modIDs.size()); - QVariantList resultList = resultData.toList(); - for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) { - QVariantMap result = iter->toMap(); - // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now - IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); - if (game - && result["id"].toInt() == game->nexusModOrganizerID() - && result["game_id"].toInt() == game->nexusGameID()) { - if (!result["voted_by_user"].toBool() && - Settings::instance().endorsementIntegration() && - !Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { - ui->actionEndorseMO->setVisible(true); - } - } else { - QString gameName = m_OrganizerCore.managedGame()->gameShortName(); - bool sameNexus = false; - for (IPluginGame *game : m_PluginContainer.plugins()) { - if (game->nexusGameID() == result["game_id"].toInt()) { - gameName = game->gameShortName(); - if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID()) - sameNexus = true; - break; - } - } - std::vector info = ModInfo::getByModID(gameName, result["id"].toInt()); - if (sameNexus) { - std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), result["id"].toInt()); - info.reserve(info.size() + mainInfo.size()); - info.insert(info.end(), mainInfo.begin(), mainInfo.end()); - } - for (auto iter = info.begin(); iter != info.end(); ++iter) { - (*iter)->setNewestVersion(result["version"].toString()); - (*iter)->setNexusDescription(result["description"].toString()); - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn() && - result.contains("voted_by_user") && - Settings::instance().endorsementIntegration()) { - // don't use endorsement info if we're not logged in or if the response doesn't contain it - (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); - } - } - } - } - - if (m_ModsToUpdate <= 0) { - statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); - break; - } - } - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } -} - -void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int) -{ - if (resultData.toBool()) { - ui->actionEndorseMO->setVisible(false); - QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!")); - } - - if (!disconnect(sender(), SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), - this, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)))) { - qCritical("failed to disconnect endorsement slot"); - } -} - -void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultData, int) -{ - QVariantList serverList = resultData.toList(); - - QList servers; - for (const QVariant &server : serverList) { - QVariantMap serverInfo = server.toMap(); - ServerInfo info; - info.name = serverInfo["Name"].toString(); - info.premium = serverInfo["IsPremium"].toBool(); - info.lastSeen = QDate::currentDate(); - info.preferred = !info.name.compare("CDN", Qt::CaseInsensitive); - // other keys: ConnectedUsers, Country, URI - servers.append(info); - } - m_OrganizerCore.settings().updateServers(servers); -} - - -void MainWindow::nxmRequestFailed(QString, int modID, int, QVariant, int, const QString &errorString) -{ - if (modID == -1) { - // must be the update-check that failed - m_ModsToUpdate = 0; - statusBar()->hide(); - } - MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); -} - - -BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, - QProgressDialog &progress) -{ - QDir().mkdir(destination); - BSA::EErrorCode result = BSA::ERROR_NONE; - QString errorFile; - - for (unsigned int i = 0; i < folder->getNumFiles(); ++i) { - BSA::File::Ptr file = folder->getFile(i); - BSA::EErrorCode res = archive.extract(file, qUtf8Printable(destination)); - if (res != BSA::ERROR_NONE) { - reportError(tr("failed to read %1: %2").arg(file->getName().c_str()).arg(res)); - result = res; - } - progress.setLabelText(file->getName().c_str()); - progress.setValue(progress.value() + 1); - QCoreApplication::processEvents(); - if (progress.wasCanceled()) { - result = BSA::ERROR_CANCELED; - } - } - - if (result != BSA::ERROR_NONE) { - if (QMessageBox::critical(this, tr("Error"), tr("failed to extract %1 (errorcode %2)").arg(errorFile).arg(result), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { - return result; - } - } - - for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) { - BSA::Folder::Ptr subFolder = folder->getSubFolder(i); - BSA::EErrorCode res = extractBSA(archive, subFolder, - destination.mid(0).append("/").append(subFolder->getName().c_str()), progress); - if (res != BSA::ERROR_NONE) { - return res; - } - } - return BSA::ERROR_NONE; -} - - -bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std::string fileName) -{ - progress.setLabelText(fileName.c_str()); - progress.setValue(percentage); - QCoreApplication::processEvents(); - return !progress.wasCanceled(); -} - - -void MainWindow::extractBSATriggered() -{ - QTreeWidgetItem *item = m_ContextItem; - QString origin; - - QString targetFolder = FileDialogMemory::getExistingDirectory("extractBSA", this, tr("Extract BSA")); - QStringList archives = {}; - if (!targetFolder.isEmpty()) { - if (!item->parent()) { - for (int i = 0; i < item->childCount(); ++i) { - archives.append(item->child(i)->text(0)); - } - origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(0))).getPath())); - } else { - origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(1))).getPath())); - archives = QStringList({ item->text(0) }); - } - - for (auto archiveName : archives) { - BSA::Archive archive; - QString archivePath = QString("%1\\%2").arg(origin).arg(archiveName); - 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; - } - - QProgressDialog progress(this); - progress.setMaximum(100); - progress.setValue(0); - progress.show(); - archive.extractAll(QDir::toNativeSeparators(targetFolder).toLocal8Bit().constData(), - boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2)); - if (result == BSA::ERROR_INVALIDHASHES) { - reportError(tr("This archive contains invalid hashes. Some files may be broken.")); - } - archive.close(); - } - } -} - - -void MainWindow::displayColumnSelection(const QPoint &pos) -{ - QMenu menu; - - // display a list of all headers as checkboxes - QAbstractItemModel *model = ui->modList->header()->model(); - for (int i = 1; i < model->columnCount(); ++i) { - QString columnName = model->headerData(i, Qt::Horizontal).toString(); - QCheckBox *checkBox = new QCheckBox(&menu); - checkBox->setText(columnName); - checkBox->setChecked(!ui->modList->header()->isSectionHidden(i)); - QWidgetAction *checkableAction = new QWidgetAction(&menu); - checkableAction->setDefaultWidget(checkBox); - menu.addAction(checkableAction); - } - menu.exec(pos); - - // view/hide columns depending on check-state - int i = 1; - for (const QAction *action : menu.actions()) { - const QWidgetAction *widgetAction = qobject_cast(action); - if (widgetAction != nullptr) { - const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); - if (checkBox != nullptr) { - ui->modList->header()->setSectionHidden(i, !checkBox->isChecked()); - } - } - ++i; - } -} - -void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos) -{ - m_ContextItem = ui->bsaList->itemAt(pos); - -// m_ContextRow = ui->bsaList->indexOfTopLevelItem(ui->bsaList->itemAt(pos)); - - QMenu menu; - menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered())); - - menu.exec(ui->bsaList->mapToGlobal(pos)); -} - -void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int) -{ - m_ArchiveListWriter.write(); - m_CheckBSATimer.start(500); -} - -void MainWindow::on_actionNotifications_triggered() -{ - updateProblemsButton(); - ProblemsDialog problems(m_PluginContainer.plugins(), this); - if (problems.hasProblems()) { - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(problems.objectName()); - if (settings.contains(key)) { - problems.restoreGeometry(settings.value(key).toByteArray()); - } - problems.exec(); - settings.setValue(key, problems.saveGeometry()); - updateProblemsButton(); - } -} - -void MainWindow::on_actionChange_Game_triggered() -{ - if (QMessageBox::question(this, tr("Are you sure?"), - tr("This will restart MO, continue?"), - QMessageBox::Yes | QMessageBox::Cancel) - == QMessageBox::Yes) { - InstanceManager::instance().clearCurrentInstance(); - qApp->exit(INT_MAX); - } -} - -void MainWindow::setCategoryListVisible(bool visible) -{ - if (visible) { - ui->categoriesGroup->show(); - ui->displayCategoriesBtn->setText(ToQString(L"\u00ab")); - } else { - ui->categoriesGroup->hide(); - ui->displayCategoriesBtn->setText(ToQString(L"\u00bb")); - } -} - -void MainWindow::on_displayCategoriesBtn_toggled(bool checked) -{ - setCategoryListVisible(checked); -} - -void MainWindow::editCategories() -{ - CategoriesDialog dialog(this); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog.objectName()); - if (settings.contains(key)) { - dialog.restoreGeometry(settings.value(key).toByteArray()); - } - if (dialog.exec() == QDialog::Accepted) { - dialog.commitChanges(); - } - settings.setValue(key, dialog.saveGeometry()); - -} - -void MainWindow::deselectFilters() -{ - ui->categoriesList->clearSelection(); -} - -void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos) -{ - QMenu menu; - menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories())); - menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters())); - - menu.exec(ui->categoriesList->mapToGlobal(pos)); -} - - -void MainWindow::updateESPLock(bool locked) -{ - QItemSelection currentSelection = ui->espList->selectionModel()->selection(); - if (currentSelection.count() == 0) { - // this path is probably useless - m_OrganizerCore.pluginList()->lockESPIndex(m_ContextRow, locked); - } else { - Q_FOREACH (const QModelIndex &idx, currentSelection.indexes()) { - if (m_OrganizerCore.pluginList()->isEnabled(mapToModel(m_OrganizerCore.pluginList(), idx).row())) { - m_OrganizerCore.pluginList()->lockESPIndex(mapToModel(m_OrganizerCore.pluginList(), idx).row(), locked); - } - } - } -} - - -void MainWindow::lockESPIndex() -{ - updateESPLock(true); -} - -void MainWindow::unlockESPIndex() -{ - updateESPLock(false); -} - - -void MainWindow::removeFromToolbar() -{ - try { - Executable &exe = m_OrganizerCore.executablesList()->find(m_ContextAction->text()); - exe.showOnToolbar(false); - } catch (const std::runtime_error&) { - qDebug("executable doesn't exist any more"); - } - - updateToolBar(); -} - - -void MainWindow::toolBar_customContextMenuRequested(const QPoint &point) -{ - QAction *action = ui->toolBar->actionAt(point); - if (action != nullptr) { - if (action->objectName().startsWith("custom_")) { - m_ContextAction = action; - QMenu menu; - menu.addAction(tr("Remove"), this, SLOT(removeFromToolbar())); - menu.exec(ui->toolBar->mapToGlobal(point)); - } - } -} - -void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) -{ - m_ContextRow = m_PluginListSortProxy->mapToSource(ui->espList->indexAt(pos)).row(); - - QMenu menu; - menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedPlugins_clicked())); - menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedPlugins_clicked())); - - menu.addSeparator(); - - menu.addAction(tr("Enable all"), m_OrganizerCore.pluginList(), SLOT(enableAll())); - menu.addAction(tr("Disable all"), m_OrganizerCore.pluginList(), SLOT(disableAll())); - - menu.addSeparator(); - - addPluginSendToContextMenu(&menu); - - QItemSelection currentSelection = ui->espList->selectionModel()->selection(); - bool hasLocked = false; - bool hasUnlocked = false; - for (const QModelIndex &idx : currentSelection.indexes()) { - int row = m_PluginListSortProxy->mapToSource(idx).row(); - if (m_OrganizerCore.pluginList()->isEnabled(row)) { - if (m_OrganizerCore.pluginList()->isESPLocked(row)) { - hasLocked = true; - } else { - hasUnlocked = true; - } - } - } - - if (hasLocked) { - menu.addAction(tr("Unlock load order"), this, SLOT(unlockESPIndex())); - } - if (hasUnlocked) { - menu.addAction(tr("Lock load order"), this, SLOT(lockESPIndex())); - } - - menu.addSeparator(); - - - QModelIndex idx = ui->espList->selectionModel()->currentIndex(); - unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(idx.data().toString())); - //this is to avoid showing the option on game files like skyrim.esm - if (modInfoIndex != UINT_MAX) { - menu.addAction(tr("Open Origin in Explorer"), this, SLOT(openOriginExplorer_clicked())); - ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); - std::vector flags = modInfo->getFlags(); - - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) { - QAction *infoAction = menu.addAction(tr("Open Origin Info..."), this, SLOT(openOriginInformation_clicked())); - menu.setDefaultAction(infoAction); - } - } - - try { - menu.exec(ui->espList->mapToGlobal(pos)); - } catch (const std::exception &e) { - reportError(tr("Exception: ").arg(e.what())); - } catch (...) { - reportError(tr("Unknown exception")); - } -} - -void MainWindow::on_groupCombo_currentIndexChanged(int index) -{ - if (m_ModListSortProxy == nullptr) { - return; - } - QAbstractProxyModel *newModel = nullptr; - switch (index) { - case 1: { - newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_CATEGORY, Qt::UserRole, - 0, Qt::UserRole + 2); - } break; - case 2: { - newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_MODID, Qt::DisplayRole, - QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE, - Qt::UserRole + 2); - } break; - default: { - newModel = nullptr; - } break; - } - - if (newModel != nullptr) { -#ifdef TEST_MODELS - new ModelTest(newModel, this); -#endif // TEST_MODELS - m_ModListSortProxy->setSourceModel(newModel); - connect(ui->modList, SIGNAL(expanded(QModelIndex)),newModel, SLOT(expanded(QModelIndex))); - connect(ui->modList, SIGNAL(collapsed(QModelIndex)), newModel, SLOT(collapsed(QModelIndex))); - connect(newModel, SIGNAL(expandItem(QModelIndex)), this, SLOT(expandModList(QModelIndex))); - } else { - m_ModListSortProxy->setSourceModel(m_OrganizerCore.modList()); - } - modFilterActive(m_ModListSortProxy->isFilterActive()); -} - -const Executable &MainWindow::getSelectedExecutable() const -{ - QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); - return m_OrganizerCore.executablesList()->find(name); -} - -Executable &MainWindow::getSelectedExecutable() -{ - QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); - return m_OrganizerCore.executablesList()->find(name); -} - -void MainWindow::on_linkButton_pressed() -{ - const Executable &selectedExecutable(getSelectedExecutable()); - - const QIcon addIcon(":/MO/gui/link"); - const QIcon removeIcon(":/MO/gui/remove"); - - const QFileInfo linkDesktopFile(getDesktopLinkfile(selectedExecutable)); - const QFileInfo linkMenuFile(getStartMenuLinkfile(selectedExecutable)); - - ui->linkButton->menu()->actions().at(static_cast(ShortcutType::Toolbar))->setIcon(selectedExecutable.isShownOnToolbar() ? removeIcon : addIcon); - ui->linkButton->menu()->actions().at(static_cast(ShortcutType::Desktop))->setIcon(linkDesktopFile.exists() ? removeIcon : addIcon); - ui->linkButton->menu()->actions().at(static_cast(ShortcutType::StartMenu))->setIcon(linkMenuFile.exists() ? removeIcon : addIcon); -} - -void MainWindow::on_showHiddenBox_toggled(bool checked) -{ - m_OrganizerCore.downloadManager()->setShowHidden(checked); -} - - -void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) -{ - SECURITY_ATTRIBUTES secAttributes; - secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - secAttributes.bInheritHandle = TRUE; - secAttributes.lpSecurityDescriptor = nullptr; - - if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { - qCritical("failed to create stdout reroute"); - } - - if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { - qCritical("failed to correctly set up the stdout reroute"); - *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; - } -} - -std::string MainWindow::readFromPipe(HANDLE stdOutRead) -{ - static const int chunkSize = 128; - std::string result; - - char buffer[chunkSize + 1]; - buffer[chunkSize] = '\0'; - - DWORD read = 1; - while (read > 0) { - if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) { - break; - } - if (read > 0) { - result.append(buffer, read); - if (read < chunkSize) { - break; - } - } - } - return result; -} - -void MainWindow::processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog) -{ - std::vector lines; - boost::split(lines, lootOut, boost::is_any_of("\r\n")); - - std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); - std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); - - for (const std::string &line : lines) { - if (line.length() > 0) { - size_t progidx = line.find("[progress]"); - size_t erroridx = line.find("[error]"); - if (progidx != std::string::npos) { - dialog.setLabelText(line.substr(progidx + 11).c_str()); - } else if (erroridx != std::string::npos) { - qWarning("%s", line.c_str()); - errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); - } else { - std::smatch match; - if (std::regex_match(line, match, exRequires)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("depends on missing \"%1\"").arg(dependency.c_str())); - } else if (std::regex_match(line, match, exIncompatible)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("incompatible with \"%1\"").arg(dependency.c_str())); - } else { - qDebug("[loot] %s", line.c_str()); - } - } - } - } -} - -void MainWindow::on_bossButton_clicked() -{ - std::string reportURL; - std::string errorMessages; - - //m_OrganizerCore.currentProfile()->writeModlistNow(); - m_OrganizerCore.savePluginList(); - //Create a backup of the load orders w/ LOOT in name - //to make sure that any sorting is easily undo-able. - //Need to figure out how I want to do that. - - bool success = false; - - try { - setEnabled(false); - ON_BLOCK_EXIT([&] () { setEnabled(true); }); - QProgressDialog dialog(this); - dialog.setLabelText(tr("Please wait while LOOT is running")); - dialog.setMaximum(0); - dialog.show(); - - QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); - - QStringList parameters; - parameters << "--game" << m_OrganizerCore.managedGame()->gameShortName() - << "--gamePath" << QString("\"%1\"").arg(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()) - << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(m_OrganizerCore.profilePath()) - << "--out" << QString("\"%1\"").arg(outPath); - - if (m_DidUpdateMasterList) { - parameters << "--skipUpdateMasterlist"; - } - HANDLE stdOutWrite = INVALID_HANDLE_VALUE; - HANDLE stdOutRead = INVALID_HANDLE_VALUE; - createStdoutPipe(&stdOutRead, &stdOutWrite); - try { - m_OrganizerCore.prepareVFS(); - } catch (const UsvfsConnectorException &e) { - qDebug(e.what()); - return; - } catch (const std::exception &e) { - QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what()); - return; - } - - HANDLE loot = startBinary(QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"), - parameters.join(" "), - qApp->applicationDirPath() + "/loot", - true, - stdOutWrite); - - // we don't use the write end - ::CloseHandle(stdOutWrite); - - m_OrganizerCore.pluginList()->clearAdditionalInformation(); - - DWORD retLen; - JOBOBJECT_BASIC_PROCESS_ID_LIST info; - HANDLE processHandle = loot; - - if (loot != INVALID_HANDLE_VALUE) { - bool isJobHandle = true; - ULONG lastProcessID; - DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); - while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { - if (isJobHandle) { - if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { - if (info.NumberOfProcessIdsInList == 0) { - qDebug("no more processes in job"); - break; - } else { - if (lastProcessID != info.ProcessIdList[0]) { - lastProcessID = info.ProcessIdList[0]; - if (processHandle != loot) { - ::CloseHandle(processHandle); - } - processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); - } - } - } else { - // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there - // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running. - // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without - // the right to break out. - if (::GetLastError() != ERROR_MORE_DATA) { - isJobHandle = false; - } - } - } - - if (dialog.wasCanceled()) { - if (isJobHandle) { - ::TerminateJobObject(loot, 1); - } else { - ::TerminateProcess(loot, 1); - } - } - - // keep processing events so the app doesn't appear dead - QCoreApplication::processEvents(); - std::string lootOut = readFromPipe(stdOutRead); - processLOOTOut(lootOut, errorMessages, dialog); - - res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); - } - - std::string remainder = readFromPipe(stdOutRead).c_str(); - if (remainder.length() > 0) { - processLOOTOut(remainder, errorMessages, dialog); - } - DWORD exitCode = 0UL; - ::GetExitCodeProcess(processHandle, &exitCode); - ::CloseHandle(processHandle); - if (exitCode != 0UL) { - reportError(tr("loot failed. Exit code was: %1").arg(exitCode)); - return; - } else { - success = true; - QFile outFile(outPath); - outFile.open(QIODevice::ReadOnly); - QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); - QJsonArray array = doc.array(); - for (auto iter = array.begin(); iter != array.end(); ++iter) { - QJsonObject pluginObj = (*iter).toObject(); - QJsonArray pluginMessages = pluginObj["messages"].toArray(); - for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { - QJsonObject msg = (*msgIter).toObject(); - m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), - QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); - } - if (pluginObj["dirty"].toString() == "yes") - m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), "dirty"); - } - - } - } else { - reportError(tr("failed to start loot")); - } - } catch (const std::exception &e) { - reportError(tr("failed to run loot: %1").arg(e.what())); - } - - if (errorMessages.length() > 0) { - QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occured"), errorMessages.c_str(), QMessageBox::Ok, this); - warn->setModal(false); - warn->show(); - } - - if (success) { - m_DidUpdateMasterList = true; - if (reportURL.length() > 0) { - m_IntegratedBrowser.setWindowTitle("LOOT Report"); - QString report(reportURL.c_str()); - QStringList temp = report.split("?"); - QUrl url = QUrl::fromLocalFile(temp.at(0)); - if (temp.size() > 1) { - url.setQuery(temp.at(1).toUtf8()); - } - m_IntegratedBrowser.openUrl(url); - } - m_OrganizerCore.refreshESPList(false); - m_OrganizerCore.savePluginList(); - } -} - - -const char *MainWindow::PATTERN_BACKUP_GLOB = ".????_??_??_??_??_??"; -const char *MainWindow::PATTERN_BACKUP_REGEX = "\\.(\\d\\d\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d)"; -const char *MainWindow::PATTERN_BACKUP_DATE = "yyyy_MM_dd_hh_mm_ss"; - - -bool MainWindow::createBackup(const QString &filePath, const QDateTime &time) -{ - QString outPath = filePath + "." + time.toString(PATTERN_BACKUP_DATE); - if (shellCopy(QStringList(filePath), QStringList(outPath), this)) { - QFileInfo fileInfo(filePath); - removeOldFiles(fileInfo.absolutePath(), fileInfo.fileName() + PATTERN_BACKUP_GLOB, 10, QDir::Name); - return true; - } else { - return false; - } -} - -void MainWindow::on_saveButton_clicked() -{ - m_OrganizerCore.savePluginList(); - QDateTime now = QDateTime::currentDateTime(); - if (createBackup(m_OrganizerCore.currentProfile()->getPluginsFileName(), now) - && createBackup(m_OrganizerCore.currentProfile()->getLoadOrderFileName(), now) - && createBackup(m_OrganizerCore.currentProfile()->getLockedOrderFileName(), now)) { - MessageDialog::showMessage(tr("Backup of load order created"), this); - } -} - -QString MainWindow::queryRestore(const QString &filePath) -{ - QFileInfo pluginFileInfo(filePath); - QString pattern = pluginFileInfo.fileName() + ".*"; - QFileInfoList files = pluginFileInfo.absoluteDir().entryInfoList(QStringList(pattern), QDir::Files, QDir::Name); - - SelectionDialog dialog(tr("Choose backup to restore"), this); - QRegExp exp(pluginFileInfo.fileName() + PATTERN_BACKUP_REGEX); - QRegExp exp2(pluginFileInfo.fileName() + "\\.(.*)"); - for(const QFileInfo &info : boost::adaptors::reverse(files)) { - if (exp.exactMatch(info.fileName())) { - QDateTime time = QDateTime::fromString(exp.cap(1), PATTERN_BACKUP_DATE); - dialog.addChoice(time.toString(), "", exp.cap(1)); - } else if (exp2.exactMatch(info.fileName())) { - dialog.addChoice(exp2.cap(1), "", exp2.cap(1)); - } - } - - if (dialog.numChoices() == 0) { - QMessageBox::information(this, tr("No Backups"), tr("There are no backups to restore")); - return QString(); - } - - if (dialog.exec() == QDialog::Accepted) { - return dialog.getChoiceData().toString(); - } else { - return QString(); - } -} - -void MainWindow::on_restoreButton_clicked() -{ - QString pluginName = m_OrganizerCore.currentProfile()->getPluginsFileName(); - QString choice = queryRestore(pluginName); - if (!choice.isEmpty()) { - QString loadOrderName = m_OrganizerCore.currentProfile()->getLoadOrderFileName(); - QString lockedName = m_OrganizerCore.currentProfile()->getLockedOrderFileName(); - if (!shellCopy(pluginName + "." + choice, pluginName, true, this) || - !shellCopy(loadOrderName + "." + choice, loadOrderName, true, this) || - !shellCopy(lockedName + "." + choice, lockedName, true, this)) { - QMessageBox::critical(this, tr("Restore failed"), - tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError()))); - } - m_OrganizerCore.refreshESPList(true); - } -} - -void MainWindow::on_saveModsButton_clicked() -{ - m_OrganizerCore.currentProfile()->writeModlistNow(true); - QDateTime now = QDateTime::currentDateTime(); - if (createBackup(m_OrganizerCore.currentProfile()->getModlistFileName(), now)) { - MessageDialog::showMessage(tr("Backup of modlist created"), this); - } -} - -void MainWindow::on_restoreModsButton_clicked() -{ - QString modlistName = m_OrganizerCore.currentProfile()->getModlistFileName(); - QString choice = queryRestore(modlistName); - if (!choice.isEmpty()) { - if (!shellCopy(modlistName + "." + choice, modlistName, true, this)) { - QMessageBox::critical(this, tr("Restore failed"), - tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError()))); - } - m_OrganizerCore.refreshModList(false); - } -} - -void MainWindow::on_actionCopy_Log_to_Clipboard_triggered() -{ - QStringList lines; - QAbstractItemModel *model = ui->logList->model(); - for (int i = 0; i < model->rowCount(); ++i) { - lines.append(QString("%1 [%2] %3").arg(model->index(i, 0).data().toString()) - .arg(model->index(i, 1).data(Qt::UserRole).toString()) - .arg(model->index(i, 1).data().toString())); - } - QApplication::clipboard()->setText(lines.join("\n")); -} - -void MainWindow::on_categoriesAndBtn_toggled(bool checked) -{ - if (checked) { - m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_AND); - } -} - -void MainWindow::on_categoriesOrBtn_toggled(bool checked) -{ - if (checked) { - m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_OR); - } -} - -void MainWindow::on_managedArchiveLabel_linkHovered(const QString&) -{ - QToolTip::showText(QCursor::pos(), - ui->managedArchiveLabel->toolTip()); -} - -void MainWindow::dragEnterEvent(QDragEnterEvent *event) -{ - //Accept copy or move drags to the download window. Link drags are not - //meaningful (Well, they are - we could drop a link in the download folder, - //but you need privileges to do that). - if (ui->downloadTab->isVisible() && - (event->proposedAction() == Qt::CopyAction || - event->proposedAction() == Qt::MoveAction) && - event->answerRect().intersects(ui->downloadTab->rect())) { - - //If I read the documentation right, this won't work under a motif windows - //manager and the check needs to be done at the drop. However, that means - //the user might be allowed to drop things which we can't sanely process - QMimeData const *data = event->mimeData(); - - if (data->hasUrls()) { - QStringList extensions = - m_OrganizerCore.installationManager()->getSupportedExtensions(); - - //This is probably OK - scan to see if these are moderately sane archive - //types - QList urls = data->urls(); - bool ok = true; - for (const QUrl &url : urls) { - if (url.isLocalFile()) { - QString local = url.toLocalFile(); - bool fok = false; - for (auto ext : extensions) { - if (local.endsWith(ext, Qt::CaseInsensitive)) { - fok = true; - break; - } - } - if (! fok) { - ok = false; - break; - } - } - } - if (ok) { - event->accept(); - } - } - } -} - -void MainWindow::dropLocalFile(const QUrl &url, const QString &outputDir, bool move) -{ - QFileInfo file(url.toLocalFile()); - if (!file.exists()) { - qWarning("invalid source file: %s", qUtf8Printable(file.absoluteFilePath())); - return; - } - QString target = outputDir + "/" + file.fileName(); - if (QFile::exists(target)) { - QMessageBox box(QMessageBox::Question, - file.fileName(), - tr("A file with the same name has already been downloaded. " - "What would you like to do?")); - box.addButton(tr("Overwrite"), QMessageBox::ActionRole); - box.addButton(tr("Rename new file"), QMessageBox::YesRole); - box.addButton(tr("Ignore file"), QMessageBox::RejectRole); - - box.exec(); - switch (box.buttonRole(box.clickedButton())) { - case QMessageBox::RejectRole: - return; - case QMessageBox::ActionRole: - break; - default: - case QMessageBox::YesRole: - target = m_OrganizerCore.downloadManager()->getDownloadFileName(file.fileName()); - break; - } - } - - bool success = false; - if (move) { - success = shellMove(file.absoluteFilePath(), target, true, this); - } else { - success = shellCopy(file.absoluteFilePath(), target, true, this); - } - if (!success) { - qCritical("file operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError()))); - } -} - -bool MainWindow::registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName) { - // register the view so it's geometry gets saved at exit - m_PersistedGeometry.push_back(std::make_pair(name, view)); - - // also, restore the geometry if it was saved before - QSettings &settings = m_OrganizerCore.settings().directInterface(); - - QString key = QString("geometry/%1").arg(name); - QByteArray data; - - if ((oldSettingName != nullptr) && settings.contains(oldSettingName)) { - data = settings.value(oldSettingName).toByteArray(); - settings.remove(oldSettingName); - } else if (settings.contains(key)) { - data = settings.value(key).toByteArray(); - } - - if (!data.isEmpty()) { - view->restoreState(data); - return true; - } else { - return false; - } -} - -void MainWindow::dropEvent(QDropEvent *event) -{ - Qt::DropAction action = event->proposedAction(); - QString outputDir = m_OrganizerCore.downloadManager()->getOutputDirectory(); - if (action == Qt::MoveAction) { - //Tell windows I'm taking control and will delete the source of a move. - event->setDropAction(Qt::TargetMoveAction); - } - for (const QUrl &url : event->mimeData()->urls()) { - if (url.isLocalFile()) { - dropLocalFile(url, outputDir, action == Qt::MoveAction); - } else { - m_OrganizerCore.downloadManager()->startDownloadURLs(QStringList() << url.url()); - } - } - event->accept(); -} - - -void MainWindow::on_clickBlankButton_clicked() -{ - deselectFilters(); -} - -void MainWindow::on_clearFiltersButton_clicked() -{ - ui->modFilterEdit->clear(); - deselectFilters(); -} - -void MainWindow::sendSelectedModsToPriority(int newPriority) -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - std::vector modsToMove; - for (auto idx : selection->selectedRows(ModList::COL_PRIORITY)) { - modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt())); - } - m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority); - } else { - m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority); - } -} - -void MainWindow::sendSelectedModsToTop_clicked() -{ - sendSelectedModsToPriority(0); -} - -void MainWindow::sendSelectedModsToBottom_clicked() -{ - sendSelectedModsToPriority(INT_MAX); -} - -void MainWindow::sendSelectedModsToPriority_clicked() -{ - bool ok; - int newPriority = QInputDialog::getInt(this, - tr("Set Priority"), tr("Set the priority of the selected mods"), - 0, 0, INT_MAX, 1, &ok); - if (!ok) return; - - sendSelectedModsToPriority(newPriority); -} - -void MainWindow::sendSelectedModsToSeparator_clicked() -{ - QStringList separators; - auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); - for (auto iter = indexesByPriority.begin(); iter != indexesByPriority.end(); iter++) { - if ((iter->second != UINT_MAX)) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(iter->second); - if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) { - separators << modInfo->name().chopped(10); // Chops the "_separator" away from the name - } - } - } - - ListDialog dialog(this); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog.objectName()); - - dialog.setWindowTitle("Select a separator..."); - dialog.setChoices(separators); - dialog.restoreGeometry(settings.value(key).toByteArray()); - if (dialog.exec() == QDialog::Accepted) { - QString result = dialog.getChoice(); - if (!result.isEmpty()) { - result += "_separator"; - - int newPriority = INT_MAX; - bool foundSection = false; - for (auto mod : m_OrganizerCore.modsSortedByProfilePriority()) { - unsigned int modIndex = ModInfo::getIndex(mod); - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if (!foundSection && result.compare(mod) == 0) { - foundSection = true; - } else if (foundSection && modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) { - newPriority = m_OrganizerCore.currentProfile()->getModPriority(modIndex); - break; - } - } - - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - std::vector modsToMove; - for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) { - modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt())); - } - m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority); - } else { - int oldPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); - if (oldPriority < newPriority) - --newPriority; - m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority); - } - } - } - settings.setValue(key, dialog.saveGeometry()); -} - -void MainWindow::on_showArchiveDataCheckBox_toggled(const bool checked) -{ - if (m_OrganizerCore.getArchiveParsing() && checked) - { - m_showArchiveData = checked; - } - else - { - m_showArchiveData = false; - } - refreshDataTree(); -} - +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "directoryentry.h" +#include "directoryrefresher.h" +#include "executableinfo.h" +#include "executableslist.h" +#include "guessedvalue.h" +#include "imodinterface.h" +#include "iplugingame.h" +#include "iplugindiagnose.h" +#include "isavegame.h" +#include "isavegameinfowidget.h" +#include "nexusinterface.h" +#include "organizercore.h" +#include "pluginlistsortproxy.h" +#include "previewgenerator.h" +#include "serverinfo.h" +#include "savegameinfo.h" +#include "spawn.h" +#include "versioninfo.h" +#include "instancemanager.h" + +#include "report.h" +#include "modlist.h" +#include "modlistsortproxy.h" +#include "qtgroupingproxy.h" +#include "profile.h" +#include "pluginlist.h" +#include "profilesdialog.h" +#include "editexecutablesdialog.h" +#include "categories.h" +#include "categoriesdialog.h" +#include "modinfodialog.h" +#include "overwriteinfodialog.h" +#include "activatemodsdialog.h" +#include "downloadlist.h" +#include "downloadlistwidget.h" +#include "messagedialog.h" +#include "installationmanager.h" +#include "lockeddialog.h" +#include "waitingonclosedialog.h" +#include "logbuffer.h" +#include "downloadlistsortproxy.h" +#include "motddialog.h" +#include "filedialogmemory.h" +#include "tutorialmanager.h" +#include "modflagicondelegate.h" +#include "genericicondelegate.h" +#include "selectiondialog.h" +#include "csvbuilder.h" +#include "savetextasdialog.h" +#include "problemsdialog.h" +#include "previewdialog.h" +#include "browserdialog.h" +#include "aboutdialog.h" +#include +#include "nxmaccessmanager.h" +#include "appconfig.h" +#include "eventfilter.h" +#include +#include +#include +#include +#include +#include +#include "localsavegames.h" +#include "listdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifndef Q_MOC_RUN +#include +#include +#include +#include +#include +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef TEST_MODELS +#include "modeltest.h" +#endif // TEST_MODELS + +#pragma warning( disable : 4428 ) + +using namespace MOBase; +using namespace MOShared; + + +MainWindow::MainWindow(QSettings &initSettings + , OrganizerCore &organizerCore + , PluginContainer &pluginContainer + , QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) + , m_WasVisible(false) + , m_Tutorial(this, "MainWindow") + , m_OldProfileIndex(-1) + , m_ModListGroupingProxy(nullptr) + , m_ModListSortProxy(nullptr) + , m_OldExecutableIndex(-1) + , m_CategoryFactory(CategoryFactory::instance()) + , m_ContextItem(nullptr) + , m_ContextAction(nullptr) + , m_ContextRow(-1) + , m_CurrentSaveView(nullptr) + , m_OrganizerCore(organizerCore) + , m_PluginContainer(pluginContainer) + , m_DidUpdateMasterList(false) + , m_ArchiveListWriter(std::bind(&MainWindow::saveArchiveList, this)) +{ + QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); + QWebEngineProfile::defaultProfile()->setHttpCacheMaximumSize(52428800); + QWebEngineProfile::defaultProfile()->setCachePath(m_OrganizerCore.settings().getCacheDirectory()); + QWebEngineProfile::defaultProfile()->setPersistentStoragePath(m_OrganizerCore.settings().getCacheDirectory()); + ui->setupUi(this); + updateWindowTitle(QString(), false); + + languageChange(m_OrganizerCore.settings().language()); + + m_CategoryFactory.loadCategories(); + + ui->logList->setModel(LogBuffer::instance()); + ui->logList->setColumnWidth(0, 100); + ui->logList->setAutoScroll(true); + ui->logList->scrollToBottom(); + ui->logList->addAction(ui->actionCopy_Log_to_Clipboard); + int splitterSize = this->size().height(); // actually total window size, but the splitter doesn't seem to return the true value + ui->topLevelSplitter->setSizes(QList() << splitterSize - 100 << 100); + connect(ui->logList->model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), + ui->logList, SLOT(scrollToBottom())); + connect(ui->logList->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), + ui->logList, SLOT(scrollToBottom())); + + m_RefreshProgress = new QProgressBar(statusBar()); + m_RefreshProgress->setTextVisible(true); + m_RefreshProgress->setRange(0, 100); + m_RefreshProgress->setValue(0); + m_RefreshProgress->setVisible(false); + statusBar()->addWidget(m_RefreshProgress, 1000); + statusBar()->clearMessage(); + statusBar()->hide(); + + updateProblemsButton(); + + // Setup toolbar + QWidget *spacer = new QWidget(ui->toolBar); + spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + QWidget *widget = ui->toolBar->widgetForAction(ui->actionTool); + QToolButton *toolBtn = qobject_cast(widget); + + if (toolBtn->menu() == nullptr) { + actionToToolButton(ui->actionTool); + } + + actionToToolButton(ui->actionHelp); + createHelpWidget(); + + actionToToolButton(ui->actionEndorseMO); + createEndorseWidget(); + ui->actionEndorseMO->setVisible(false); + + for (QAction *action : ui->toolBar->actions()) { + if (action->isSeparator()) { + // insert spacers + ui->toolBar->insertWidget(action, spacer); + m_Sep = action; + // m_Sep would only use the last separator anyway, and we only have the one anyway? + break; + } + } + + TaskProgressManager::instance().tryCreateTaskbar(); + + // set up mod list + m_ModListSortProxy = m_OrganizerCore.createModListProxyModel(); + + ui->modList->setModel(m_ModListSortProxy); + + GenericIconDelegate *contentDelegate = new GenericIconDelegate(ui->modList, Qt::UserRole + 3, ModList::COL_CONTENT, 150); + connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), contentDelegate, SLOT(columnResized(int,int,int))); + ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder); + ModFlagIconDelegate *flagDelegate = new ModFlagIconDelegate(ui->modList, ModList::COL_FLAGS, 120); + connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), flagDelegate, SLOT(columnResized(int,int,int))); + ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate); + ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate); + ui->modList->header()->installEventFilter(m_OrganizerCore.modList()); + connect(ui->modList->header(), SIGNAL(sectionResized(int, int, int)), this, SLOT(modListSectionResized(int, int, int))); + + bool modListAdjusted = registerWidgetState(ui->modList->objectName(), ui->modList->header(), "mod_list_state"); + + if (modListAdjusted) { + // hack: force the resize-signal to be triggered because restoreState doesn't seem to do that + for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) { + int sectionSize = ui->modList->header()->sectionSize(column); + ui->modList->header()->resizeSection(column, sectionSize + 1); + ui->modList->header()->resizeSection(column, sectionSize); + } + } else { + // hide these columns by default + ui->modList->header()->setSectionHidden(ModList::COL_CONTENT, true); + ui->modList->header()->setSectionHidden(ModList::COL_MODID, true); + ui->modList->header()->setSectionHidden(ModList::COL_GAME, true); + ui->modList->header()->setSectionHidden(ModList::COL_INSTALLTIME, true); + ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true); + } + + ui->modList->header()->setSectionHidden(ModList::COL_NAME, false); // prevent the name-column from being hidden + ui->modList->installEventFilter(m_OrganizerCore.modList()); + + // set up plugin list + m_PluginListSortProxy = m_OrganizerCore.createPluginListProxyModel(); + + ui->espList->setModel(m_PluginListSortProxy); + ui->espList->sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder); + ui->espList->setItemDelegateForColumn(PluginList::COL_FLAGS, new GenericIconDelegate(ui->espList)); + ui->espList->installEventFilter(m_OrganizerCore.pluginList()); + + ui->bsaList->setLocalMoveOnly(true); + + initDownloadView(); + bool pluginListAdjusted = registerWidgetState(ui->espList->objectName(), ui->espList->header(), "plugin_list_state"); + registerWidgetState(ui->dataTree->objectName(), ui->dataTree->header()); + registerWidgetState(ui->downloadView->objectName(), + ui->downloadView->header()); + + ui->splitter->setStretchFactor(0, 3); + ui->splitter->setStretchFactor(1, 2); + + resizeLists(modListAdjusted, pluginListAdjusted); + + QMenu *linkMenu = new QMenu(this); + linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Toolbar"), this, SLOT(linkToolbar())); + linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this, SLOT(linkDesktop())); + linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); + ui->linkButton->setMenu(linkMenu); + + QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn); + initModListContextMenu(listOptionsMenu); + ui->listOptionsBtn->setMenu(listOptionsMenu); + connect(ui->listOptionsBtn, SIGNAL(pressed()), this, SLOT(on_listOptionsBtn_pressed())); + + ui->openFolderMenu->setMenu(openFolderMenu()); + + ui->savegameList->installEventFilter(this); + ui->savegameList->setMouseTracking(true); + + // don't allow mouse wheel to switch grouping, too many people accidentally + // turn on grouping and then don't understand what happened + EventFilter *noWheel + = new EventFilter(this, [](QObject *, QEvent *event) -> bool { + return event->type() == QEvent::Wheel; + }); + + ui->groupCombo->installEventFilter(noWheel); + ui->profileBox->installEventFilter(noWheel); + + if (organizerCore.managedGame()->sortMechanism() == MOBase::IPluginGame::SortMechanism::NONE) { + ui->bossButton->setDisabled(true); + ui->bossButton->setToolTip(tr("There is no supported sort mechanism for this game. You will probably have to use a third-party tool.")); + } + + connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*))); + + connect(ui->modList, SIGNAL(dropModeUpdate(bool)), m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool))); + + connect(ui->modList->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(modlistSelectionChanged(QModelIndex,QModelIndex))); + connect(m_ModListSortProxy, SIGNAL(filterActive(bool)), this, SLOT(modFilterActive(bool))); + connect(m_ModListSortProxy, SIGNAL(layoutChanged()), this, SLOT(updateModCount())); + connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString))); + + connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), m_PluginListSortProxy, SLOT(updateFilter(QString))); + connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString))); + + connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*))); + + connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed())); + connect(m_OrganizerCore.directoryRefresher(), SIGNAL(progress(int)), this, SLOT(refresher_progress(int))); + connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString))); + + connect(&m_SavesWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(refreshSavesIfOpen())); + + connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString))); + connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString))); + + connect(m_OrganizerCore.updater(), SIGNAL(restart()), this, SLOT(close())); + connect(m_OrganizerCore.updater(), SIGNAL(updateAvailable()), this, SLOT(updateAvailable())); + connect(m_OrganizerCore.updater(), SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString))); + + connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); + connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int))); + connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusLogin())); + connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); + connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), + this, SLOT(updateWindowTitle(const QString&, bool))); + + connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); + connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int))); + connect(ui->modList->header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), this, SLOT(modListSortIndicatorChanged(int,Qt::SortOrder))); + connect(ui->toolBar, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(toolBar_customContextMenuRequested(QPoint))); + + connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled); + connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close); + + connect(&m_IntegratedBrowser, SIGNAL(requestDownload(QUrl,QNetworkReply*)), &m_OrganizerCore, SLOT(requestDownload(QUrl,QNetworkReply*))); + + connect(this, SIGNAL(styleChanged(QString)), this, SLOT(updateStyle(QString))); + + m_CheckBSATimer.setSingleShot(true); + connect(&m_CheckBSATimer, SIGNAL(timeout()), this, SLOT(checkBSAList())); + + connect(ui->espList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(esplistSelectionsChanged(QItemSelection))); + connect(ui->modList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(modlistSelectionsChanged(QItemSelection))); + + new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated())); + new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated())); + + new QShortcut(QKeySequence::Refresh, this, SLOT(refreshProfile_activated())); + + new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F), this, SLOT(search_activated())); + new QShortcut(QKeySequence(Qt::Key_Escape), this, SLOT(searchClear_activated())); + + m_UpdateProblemsTimer.setSingleShot(true); + connect(&m_UpdateProblemsTimer, SIGNAL(timeout()), this, SLOT(updateProblemsButton())); + + m_SaveMetaTimer.setSingleShot(false); + connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); + m_SaveMetaTimer.start(5000); + + setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); + FileDialogMemory::restore(initSettings); + + fixCategories(); + + m_StartTime = QTime::currentTime(); + + m_Tutorial.expose("modList", m_OrganizerCore.modList()); + m_Tutorial.expose("espList", m_OrganizerCore.pluginList()); + + m_OrganizerCore.setUserInterface(this, this); + for (const QString &fileName : m_PluginContainer.pluginFileNames()) { + installTranslator(QFileInfo(fileName).baseName()); + } + + registerPluginTools(m_PluginContainer.plugins()); + + for (IPluginModPage *modPagePlugin : m_PluginContainer.plugins()) { + registerModPage(modPagePlugin); + } + + // refresh profiles so the current profile can be activated + refreshProfiles(false); + + ui->profileBox->setCurrentText(m_OrganizerCore.currentProfile()->name()); + + if (m_OrganizerCore.getArchiveParsing()) + { + ui->showArchiveDataCheckBox->setCheckState(Qt::Checked); + ui->showArchiveDataCheckBox->setEnabled(true); + m_showArchiveData = true; + } + else + { + ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked); + ui->showArchiveDataCheckBox->setEnabled(false); + m_showArchiveData = false; + } + + refreshExecutablesList(); + updateToolBar(); + + for (QAction *action : ui->toolBar->actions()) { + // set the name of the widget to the name of the action to allow styling + QWidget *actionWidget = ui->toolBar->widgetForAction(action); + actionWidget->setObjectName(action->objectName()); + actionWidget->style()->unpolish(actionWidget); + actionWidget->style()->polish(actionWidget); + } + + updatePluginCount(); + updateModCount(); +} + + +MainWindow::~MainWindow() +{ + try { + cleanup(); + + m_PluginContainer.setUserInterface(nullptr, nullptr); + m_OrganizerCore.setUserInterface(nullptr, nullptr); + m_IntegratedBrowser.close(); + delete ui; + } catch (std::exception &e) { + QMessageBox::critical(nullptr, tr("Crash on exit"), + tr("MO crashed while exiting. Some settings may not be saved.\n\nError: %1").arg(e.what()), + QMessageBox::Ok); + } +} + + +void MainWindow::updateWindowTitle(const QString &accountName, bool premium) +{ + QString title = QString("%1 Mod Organizer v%2").arg( + m_OrganizerCore.managedGame()->gameName(), + m_OrganizerCore.getVersion().displayString(3)); + + if (!accountName.isEmpty()) { + title.append(QString(" (%1%2)").arg(accountName, premium ? "*" : "")); + } + + this->setWindowTitle(title); +} + + +void MainWindow::disconnectPlugins() +{ + if (ui->actionTool->menu() != nullptr) { + ui->actionTool->menu()->clear(); + } +} + + +void MainWindow::resizeLists(bool modListCustom, bool pluginListCustom) +{ + if (!modListCustom) { + // resize mod list to fit content + for (int i = 0; i < ui->modList->header()->count(); ++i) { + ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch); + } + + // ensure the columns aren't so small you can't see them any more + for (int i = 0; i < ui->modList->header()->count(); ++i) { + if (ui->modList->header()->sectionSize(i) < 10) { + ui->modList->header()->resizeSection(i, 10); + } + } + + if (!pluginListCustom) { + // resize plugin list to fit content + for (int i = 0; i < ui->espList->header()->count(); ++i) { + ui->espList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + ui->espList->header()->setSectionResizeMode(0, QHeaderView::Stretch); + } +} + + +void MainWindow::allowListResize() +{ + // allow resize on mod list + for (int i = 0; i < ui->modList->header()->count(); ++i) { + ui->modList->header()->setSectionResizeMode(i, QHeaderView::Interactive); + } + //ui->modList->header()->setSectionResizeMode(ui->modList->header()->count() - 1, QHeaderView::Stretch); + ui->modList->header()->setStretchLastSection(true); + + + // allow resize on plugin list + for (int i = 0; i < ui->espList->header()->count(); ++i) { + ui->espList->header()->setSectionResizeMode(i, QHeaderView::Interactive); + } + //ui->espList->header()->setSectionResizeMode(ui->espList->header()->count() - 1, QHeaderView::Stretch); + ui->espList->header()->setStretchLastSection(true); +} + +void MainWindow::updateStyle(const QString&) +{ + // no effect? + ensurePolished(); +} + +void MainWindow::resizeEvent(QResizeEvent *event) +{ + m_Tutorial.resize(event->size()); + QMainWindow::resizeEvent(event); +} + + +static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex idx) +{ + QModelIndex result = idx; + const QAbstractItemModel *model = idx.model(); + while (model != targetModel) { + if (model == nullptr) { + return QModelIndex(); + } + const QAbstractProxyModel *proxyModel = qobject_cast(model); + if (proxyModel == nullptr) { + return QModelIndex(); + } + result = proxyModel->mapToSource(result); + model = proxyModel->sourceModel(); + } + return result; +} + + +void MainWindow::actionToToolButton(QAction *&sourceAction) +{ + QToolButton *button = new QToolButton(ui->toolBar); + button->setObjectName(sourceAction->objectName()); + button->setIcon(sourceAction->icon()); + button->setText(sourceAction->text()); + button->setPopupMode(QToolButton::InstantPopup); + button->setToolButtonStyle(ui->toolBar->toolButtonStyle()); + button->setToolTip(sourceAction->toolTip()); + button->setShortcut(sourceAction->shortcut()); + QMenu *buttonMenu = new QMenu(sourceAction->text(), button); + button->setMenu(buttonMenu); + QAction *newAction = ui->toolBar->insertWidget(sourceAction, button); + newAction->setObjectName(sourceAction->objectName()); + newAction->setIcon(sourceAction->icon()); + newAction->setText(sourceAction->text()); + newAction->setToolTip(sourceAction->toolTip()); + newAction->setShortcut(sourceAction->shortcut()); + ui->toolBar->removeAction(sourceAction); + sourceAction->deleteLater(); + sourceAction = newAction; +} + +void MainWindow::updateToolBar() +{ + for (QAction *action : ui->toolBar->actions()) { + if (action->objectName().startsWith("custom__")) { + ui->toolBar->removeAction(action); + action->deleteLater(); + } + } + + std::vector::iterator begin, end; + m_OrganizerCore.executablesList()->getExecutables(begin, end); + for (auto iter = begin; iter != end; ++iter) { + if (iter->isShownOnToolbar()) { + QAction *exeAction = new QAction(iconForExecutable(iter->m_BinaryInfo.filePath()), + iter->m_Title, + ui->toolBar); + exeAction->setObjectName(QString("custom__") + iter->m_Title); + if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) { + qDebug("failed to connect trigger?"); + } + ui->toolBar->insertAction(m_Sep, exeAction); + } + } +} + + +void MainWindow::scheduleUpdateButton() +{ + if (!m_UpdateProblemsTimer.isActive()) { + m_UpdateProblemsTimer.start(1000); + } +} + + +void MainWindow::updateProblemsButton() +{ + size_t numProblems = checkForProblems(); + if (numProblems > 0) { + ui->actionNotifications->setEnabled(true); + ui->actionNotifications->setIconText(tr("Notifications")); + ui->actionNotifications->setToolTip(tr("There are notifications to read")); + + QPixmap mergedIcon = QPixmap(":/MO/gui/warning").scaled(64, 64); + { + QPainter painter(&mergedIcon); + 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())); + } + ui->actionNotifications->setIcon(QIcon(mergedIcon)); + } else { + ui->actionNotifications->setEnabled(false); + ui->actionNotifications->setIconText(tr("No Notifications")); + ui->actionNotifications->setToolTip(tr("There are no notifications")); + ui->actionNotifications->setIcon(QIcon(":/MO/gui/warning")); + } +} + + +bool MainWindow::errorReported(QString &logFile) +{ + QDir dir(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath())); + QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"), + QDir::Files, QDir::Name | QDir::Reversed); + + if (files.count() > 0) { + logFile = files.at(0).absoluteFilePath(); + QFile file(logFile); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + char buffer[1024]; + int line = 0; + while (!file.atEnd()) { + file.readLine(buffer, 1024); + if (strncmp(buffer, "ERROR", 5) == 0) { + return true; + } + + // prevent this function from taking forever + if (line++ >= 50000) { + break; + } + } + } + } + + return false; +} + + +size_t MainWindow::checkForProblems() +{ + size_t numProblems = 0; + for (IPluginDiagnose *diagnose : m_PluginContainer.plugins()) { + numProblems += diagnose->activeProblems().size(); + } + return numProblems; +} + +void MainWindow::about() +{ + AboutDialog dialog(m_OrganizerCore.getVersion().displayString(3), this); + connect(&dialog, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); + dialog.exec(); +} + + +void MainWindow::createEndorseWidget() +{ + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionEndorseMO)); + QMenu *buttonMenu = toolBtn->menu(); + if (buttonMenu == nullptr) { + return; + } + buttonMenu->clear(); + + QAction *endorseAction = new QAction(tr("Endorse"), buttonMenu); + connect(endorseAction, SIGNAL(triggered()), this, SLOT(on_actionEndorseMO_triggered())); + buttonMenu->addAction(endorseAction); + + QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu); + connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(wontEndorse())); + buttonMenu->addAction(wontEndorseAction); +} + + +void MainWindow::createHelpWidget() +{ + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionHelp)); + QMenu *buttonMenu = toolBtn->menu(); + if (buttonMenu == nullptr) { + return; + } + buttonMenu->clear(); + + QAction *helpAction = new QAction(tr("Help on UI"), buttonMenu); + connect(helpAction, SIGNAL(triggered()), this, SLOT(helpTriggered())); + buttonMenu->addAction(helpAction); + + QAction *wikiAction = new QAction(tr("Documentation"), buttonMenu); + connect(wikiAction, SIGNAL(triggered()), this, SLOT(wikiTriggered())); + buttonMenu->addAction(wikiAction); + + QAction *discordAction = new QAction(tr("Chat on Discord"), buttonMenu); + connect(discordAction, SIGNAL(triggered()), this, SLOT(discordTriggered())); + buttonMenu->addAction(discordAction); + + QAction *issueAction = new QAction(tr("Report Issue"), buttonMenu); + connect(issueAction, SIGNAL(triggered()), this, SLOT(issueTriggered())); + buttonMenu->addAction(issueAction); + + QMenu *tutorialMenu = new QMenu(tr("Tutorials"), buttonMenu); + + typedef std::vector > ActionList; + + ActionList tutorials; + + QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); + while (dirIter.hasNext()) { + dirIter.next(); + QString fileName = dirIter.fileName(); + + QFile file(dirIter.filePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open " << fileName; + continue; + } + QString firstLine = QString::fromUtf8(file.readLine()); + if (firstLine.startsWith("//TL")) { + QStringList params = firstLine.mid(4).trimmed().split('#'); + if (params.size() != 2) { + qCritical() << "invalid header line for tutorial " << fileName << " expected 2 parameters"; + continue; + } + QAction *tutAction = new QAction(params.at(0), tutorialMenu); + tutAction->setData(fileName); + tutorials.push_back(std::make_pair(params.at(1).toInt(), tutAction)); + } + } + + std::sort(tutorials.begin(), tutorials.end(), + [] (const ActionList::value_type &LHS, const ActionList::value_type &RHS) { + return LHS.first < RHS.first; } ); + + for (auto iter = tutorials.begin(); iter != tutorials.end(); ++iter) { + connect(iter->second, SIGNAL(triggered()), this, SLOT(tutorialTriggered())); + tutorialMenu->addAction(iter->second); + } + + buttonMenu->addMenu(tutorialMenu); + buttonMenu->addAction(tr("About"), this, SLOT(about())); + buttonMenu->addAction(tr("About Qt"), qApp, SLOT(aboutQt())); +} + +void MainWindow::modFilterActive(bool filterActive) +{ + ui->clearFiltersButton->setVisible(filterActive); + if (filterActive) { +// m_OrganizerCore.modList()->setOverwriteMarkers(std::set(), std::set()); + ui->modList->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); + ui->activeModsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }"); + } else if (ui->groupCombo->currentIndex() != 0) { + ui->modList->setStyleSheet("QTreeView { border: 2px ridge #337733; }"); + ui->activeModsCounter->setStyleSheet(""); + } else { + ui->modList->setStyleSheet(""); + ui->activeModsCounter->setStyleSheet(""); + } +} + +void MainWindow::espFilterChanged(const QString &filter) +{ + if (!filter.isEmpty()) { + ui->espList->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); + ui->activePluginsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }"); + } else { + ui->espList->setStyleSheet(""); + ui->activePluginsCounter->setStyleSheet(""); + } + updatePluginCount(); +} + +void MainWindow::downloadFilterChanged(const QString &filter) +{ + if (!filter.isEmpty()) { + ui->downloadView->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); + } else { + ui->downloadView->setStyleSheet(""); + } +} + +void MainWindow::expandModList(const QModelIndex &index) +{ + QAbstractItemModel *model = ui->modList->model(); +#pragma message("why is this so complicated? mapping the index doesn't work, probably a bug in QtGroupingProxy?") + for (int i = 0; i < model->rowCount(); ++i) { + QModelIndex targetIdx = model->index(i, 0); + if (model->data(targetIdx).toString() == index.data().toString()) { + ui->modList->expand(targetIdx); + break; + } + } +} + + +bool MainWindow::addProfile() +{ + QComboBox *profileBox = findChild("profileBox"); + bool okClicked = false; + + QString name = QInputDialog::getText(this, tr("Name"), + tr("Please enter a name for the new profile"), + QLineEdit::Normal, QString(), &okClicked); + if (okClicked && (name.size() > 0)) { + try { + profileBox->addItem(name); + profileBox->setCurrentIndex(profileBox->count() - 1); + return true; + } catch (const std::exception& e) { + reportError(tr("failed to create profile: %1").arg(e.what())); + return false; + } + } else { + return false; + } +} + +void MainWindow::hookUpWindowTutorials() +{ + QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); + while (dirIter.hasNext()) { + dirIter.next(); + QString fileName = dirIter.fileName(); + QFile file(dirIter.filePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open " << fileName; + continue; + } + QString firstLine = QString::fromUtf8(file.readLine()); + if (firstLine.startsWith("//WIN")) { + QString windowName = firstLine.mid(6).trimmed(); + if (!m_OrganizerCore.settings().directInterface().value("CompletedWindowTutorials/" + windowName, false).toBool()) { + TutorialManager::instance().activateTutorial(windowName, fileName); + } + } + } +} + +void MainWindow::showEvent(QShowEvent *event) +{ + refreshFilters(); + + QMainWindow::showEvent(event); + + if (!m_WasVisible) { + // only the first time the window becomes visible + m_Tutorial.registerControl(); + + hookUpWindowTutorials(); + + if (m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) { + QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial()); + if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) { + if (QMessageBox::question(this, tr("Show tutorial?"), + tr("You are starting Mod Organizer for the first time. " + "Do you want to show a tutorial of its basic features? If you choose " + "no you can always start the tutorial from the \"Help\"-menu."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial); + } + } else { + qCritical() << firstStepsTutorial << " missing"; + QPoint pos = ui->toolBar->mapToGlobal(QPoint()); + pos.rx() += ui->toolBar->width() / 2; + pos.ry() += ui->toolBar->height(); + QWhatsThis::showText(pos, + QObject::tr("Please use \"Help\" from the toolbar to get usage instructions to all elements")); + } + + m_OrganizerCore.settings().directInterface().setValue("first_start", false); + } + + // this has no visible impact when called before the ui is visible + int grouping = m_OrganizerCore.settings().directInterface().value("group_state").toInt(); + ui->groupCombo->setCurrentIndex(grouping); + + allowListResize(); + + m_OrganizerCore.settings().registerAsNXMHandler(false); + m_WasVisible = true; + updateProblemsButton(); + } +} + + +void MainWindow::closeEvent(QCloseEvent* event) +{ + m_closing = true; + + if (m_OrganizerCore.downloadManager()->downloadsInProgressNoPause()) { + if (QMessageBox::question(this, tr("Downloads in progress"), + tr("There are still downloads in progress, do you really want to quit?"), + QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) { + event->ignore(); + return; + } else { + m_OrganizerCore.downloadManager()->pauseAll(); + } + } + + std::vector hiddenList; + hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); + HANDLE injected_process_still_running = m_OrganizerCore.findAndOpenAUSVFSProcess(hiddenList, GetCurrentProcessId()); + if (injected_process_still_running != INVALID_HANDLE_VALUE) + { + m_OrganizerCore.waitForApplication(injected_process_still_running); + if (!m_closing) { // if operation cancelled + event->ignore(); + return; + } + } + + setCursor(Qt::WaitCursor); +} + +void MainWindow::cleanup() +{ + if (ui->logList->model() != nullptr) { + disconnect(ui->logList->model(), nullptr, nullptr, nullptr); + ui->logList->setModel(nullptr); + } + + QWebEngineProfile::defaultProfile()->clearAllVisitedLinks(); + m_IntegratedBrowser.close(); + m_SaveMetaTimer.stop(); + m_MetaSave.waitForFinished(); +} + + +void MainWindow::setBrowserGeometry(const QByteArray &geometry) +{ + m_IntegratedBrowser.restoreGeometry(geometry); +} + +void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem) +{ + QString const &save = newItem->data(Qt::UserRole).toString(); + if (m_CurrentSaveView == nullptr) { + IPluginGame const *game = m_OrganizerCore.managedGame(); + SaveGameInfo const *info = game->feature(); + if (info != nullptr) { + m_CurrentSaveView = info->getSaveGameWidget(this); + } + if (m_CurrentSaveView == nullptr) { + return; + } + } + m_CurrentSaveView->setSave(save); + + QRect screenRect = QApplication::desktop()->availableGeometry(m_CurrentSaveView); + + QPoint pos = QCursor::pos(); + if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) { + pos.rx() -= (m_CurrentSaveView->width() + 2); + } else { + pos.rx() += 5; + } + + if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) { + pos.ry() -= (m_CurrentSaveView->height() + 10); + } else { + pos.ry() += 20; + } + m_CurrentSaveView->move(pos); + + m_CurrentSaveView->show(); + m_CurrentSaveView->setProperty("displayItem", qVariantFromValue(static_cast(newItem))); + + ui->savegameList->activateWindow(); +} + + +void MainWindow::saveSelectionChanged(QListWidgetItem *newItem) +{ + if (newItem == nullptr) { + hideSaveGameInfo(); + } else if (m_CurrentSaveView == nullptr || newItem != m_CurrentSaveView->property("displayItem").value()) { + displaySaveGameInfo(newItem); + } +} + + +void MainWindow::hideSaveGameInfo() +{ + if (m_CurrentSaveView != nullptr) { + m_CurrentSaveView->deleteLater(); + m_CurrentSaveView = nullptr; + } +} + +bool MainWindow::eventFilter(QObject *object, QEvent *event) +{ + if ((object == ui->savegameList) && + ((event->type() == QEvent::Leave) || (event->type() == QEvent::WindowDeactivate))) { + hideSaveGameInfo(); + } + return false; +} + + +void MainWindow::toolPluginInvoke() +{ + QAction *triggeredAction = qobject_cast(sender()); + IPluginTool *plugin = qobject_cast(triggeredAction->data().value()); + if (plugin != nullptr) { + try { + plugin->display(); + } catch (const std::exception &e) { + reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what())); + } catch (...) { + reportError(tr("Plugin \"%1\" failed").arg(plugin->name())); + } + } +} + +void MainWindow::modPagePluginInvoke() +{ + QAction *triggeredAction = qobject_cast(sender()); + IPluginModPage *plugin = qobject_cast(triggeredAction->data().value()); + if (plugin != nullptr) { + if (plugin->useIntegratedBrowser()) { + m_IntegratedBrowser.setWindowTitle(plugin->displayName()); + m_IntegratedBrowser.openUrl(plugin->pageURL()); + } else { + QDesktopServices::openUrl(QUrl(plugin->pageURL())); + } + } +} + +void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu) +{ + if (name.isEmpty()) + name = tool->displayName(); + + QAction *action = new QAction(tool->icon(), name, ui->toolBar); + action->setToolTip(tool->tooltip()); + tool->setParentWidget(this); + action->setData(qVariantFromValue((QObject*)tool)); + connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke()), Qt::QueuedConnection); + + if (menu == nullptr) { + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionTool)); + toolBtn->menu()->addAction(action); + } else { + menu->addAction(action); + } +} + +void MainWindow::registerPluginTools(std::vector toolPlugins) +{ + // Sort the plugins by display name + std::sort(toolPlugins.begin(), toolPlugins.end(), + [](IPluginTool *left, IPluginTool *right) { + return left->displayName().toLower() < right->displayName().toLower(); + } + ); + + // Group the plugins into submenus + QMap>> submenuMap; + for (auto toolPlugin : toolPlugins) { + QStringList toolName = toolPlugin->displayName().split("/"); + QString submenu = toolName[0]; + toolName.pop_front(); + submenuMap[submenu].append(QPair(toolName.join("/"), toolPlugin)); + } + + // Start registering plugins + for (auto submenuKey : submenuMap.keys()) { + if (submenuMap[submenuKey].length() > 1) { + QMenu *submenu = new QMenu(submenuKey, this); + for (auto info : submenuMap[submenuKey]) { + registerPluginTool(info.second, info.first, submenu); + } + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionTool)); + toolBtn->menu()->addMenu(submenu); + } + else { + registerPluginTool(submenuMap[submenuKey].front().second); + } + } +} + +void MainWindow::registerModPage(IPluginModPage *modPage) +{ + // turn the browser action into a drop-down menu if necessary + if (ui->actionNexus->menu() == nullptr) { + QAction *nexusAction = ui->actionNexus; + // TODO: use a different icon for nexus! + ui->actionNexus = new QAction(nexusAction->icon(), tr("Browse Mod Page"), ui->toolBar); + ui->toolBar->insertAction(nexusAction, ui->actionNexus); + ui->toolBar->removeAction(nexusAction); + actionToToolButton(ui->actionNexus); + + QToolButton *browserBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionNexus)); + browserBtn->menu()->addAction(nexusAction); + } + + QAction *action = new QAction(modPage->icon(), modPage->displayName(), ui->toolBar); + modPage->setParentWidget(this); + action->setData(qVariantFromValue(reinterpret_cast(modPage))); + + connect(action, SIGNAL(triggered()), this, SLOT(modPagePluginInvoke()), Qt::QueuedConnection); + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionNexus)); + toolBtn->menu()->addAction(action); +} + + +void MainWindow::startExeAction() +{ + QAction *action = qobject_cast(sender()); + if (action != nullptr) { + const Executable &selectedExecutable(m_OrganizerCore.executablesList()->find(action->text())); + QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.m_Title).toString(); + auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.m_Title); + if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.m_Title)) { + forcedLibraries.clear(); + } + m_OrganizerCore.spawnBinary( + selectedExecutable.m_BinaryInfo, selectedExecutable.m_Arguments, + selectedExecutable.m_WorkingDirectory.length() != 0 + ? selectedExecutable.m_WorkingDirectory + : selectedExecutable.m_BinaryInfo.absolutePath(), + selectedExecutable.m_SteamAppID, + customOverwrite, + forcedLibraries); + } else { + qCritical("not an action?"); + } +} + + +void MainWindow::setExecutableIndex(int index) +{ + QComboBox *executableBox = findChild("executablesListBox"); + + if ((index != 0) && (executableBox->count() > index)) { + executableBox->setCurrentIndex(index); + } else { + executableBox->setCurrentIndex(1); + } +} + +void MainWindow::activateSelectedProfile() +{ + m_OrganizerCore.setCurrentProfile(ui->profileBox->currentText()); + + m_ModListSortProxy->setProfile(m_OrganizerCore.currentProfile()); + + refreshSaveList(); + m_OrganizerCore.refreshModList(); + updateModCount(); + updatePluginCount(); +} + +void MainWindow::on_profileBox_currentIndexChanged(int index) +{ + if (ui->profileBox->isEnabled()) { + int previousIndex = m_OldProfileIndex; + m_OldProfileIndex = index; + + if ((previousIndex != -1) && + (m_OrganizerCore.currentProfile() != nullptr) && + m_OrganizerCore.currentProfile()->exists()) { + m_OrganizerCore.saveCurrentLists(); + } + + // ensure the new index is valid + if (index < 0 || index >= ui->profileBox->count()) { + qDebug("invalid profile index, using last profile"); + ui->profileBox->setCurrentIndex(ui->profileBox->count() - 1); + } + + if (ui->profileBox->currentIndex() == 0) { + ui->profileBox->setCurrentIndex(previousIndex); + ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec(); + while (!refreshProfiles()) { + ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec(); + } + } else { + activateSelectedProfile(); + } + + LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature(); + if (saveGames != nullptr) { + if (saveGames->prepareProfile(m_OrganizerCore.currentProfile())) + refreshSaveList(); + } + + BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature(); + if (invalidation != nullptr) { + if (invalidation->prepareProfile(m_OrganizerCore.currentProfile())) + QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh())); + } + } +} + +void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon) +{ + bool isDirectory = true; + //QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); + //QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); + + std::wostringstream temp; + temp << directorySoFar << "\\" << directoryEntry.getName(); + { + std::vector::const_iterator current, end; + directoryEntry.getSubDirectories(current, end); + for (; current != end; ++current) { + QString pathName = ToQString((*current)->getName()); + QStringList columns(pathName); + columns.append(""); + if (!(*current)->isEmpty()) { + QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns); + directoryChild->setData(0, Qt::DecorationRole, *folderIcon); + directoryChild->setData(0, Qt::UserRole + 3, isDirectory); + + if (conflictsOnly || !m_showArchiveData) { + updateTo(directoryChild, temp.str(), **current, conflictsOnly, fileIcon, folderIcon); + if (directoryChild->childCount() != 0) { + subTree->addChild(directoryChild); + } + else { + delete directoryChild; + } + } + else { + QTreeWidgetItem *onDemandLoad = new QTreeWidgetItem(QStringList()); + onDemandLoad->setData(0, Qt::UserRole + 0, "__loaded_on_demand__"); + onDemandLoad->setData(0, Qt::UserRole + 1, ToQString(temp.str())); + onDemandLoad->setData(0, Qt::UserRole + 2, conflictsOnly); + directoryChild->addChild(onDemandLoad); + subTree->addChild(directoryChild); + } + } + else { + QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns); + directoryChild->setData(0, Qt::DecorationRole, *folderIcon); + directoryChild->setData(0, Qt::UserRole + 3, isDirectory); + subTree->addChild(directoryChild); + } + } + } + + + isDirectory = false; + { + for (const FileEntry::Ptr current : directoryEntry.getFiles()) { + if (conflictsOnly && (current->getAlternatives().size() == 0)) { + continue; + } + + bool isArchive = false; + int originID = current->getOrigin(isArchive); + if (!m_showArchiveData && isArchive) { + continue; + } + + QString fileName = ToQString(current->getName()); + QStringList columns(fileName); + FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID); + QString source("data"); + unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName())); + if (modIndex != UINT_MAX) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + source = modInfo->name(); + } + + std::pair archive = current->getArchive(); + if (archive.first.length() != 0) { + source.append(" (").append(ToQString(archive.first)).append(")"); + } + columns.append(source); + QTreeWidgetItem *fileChild = new QTreeWidgetItem(columns); + if (isArchive) { + QFont font = fileChild->font(0); + font.setItalic(true); + fileChild->setFont(0, font); + fileChild->setFont(1, font); + } else if (fileName.endsWith(ModInfo::s_HiddenExt)) { + QFont font = fileChild->font(0); + font.setStrikeOut(true); + fileChild->setFont(0, font); + fileChild->setFont(1, font); + } + fileChild->setData(0, Qt::UserRole, ToQString(current->getFullPath())); + fileChild->setData(0, Qt::DecorationRole, *fileIcon); + fileChild->setData(0, Qt::UserRole + 3, isDirectory); + fileChild->setData(0, Qt::UserRole + 1, isArchive); + fileChild->setData(1, Qt::UserRole, source); + fileChild->setData(1, Qt::UserRole + 1, originID); + + std::vector>> alternatives = current->getAlternatives(); + + if (!alternatives.empty()) { + std::wostringstream altString; + altString << ToWString(tr("Also in:
    ")); + for (std::vector>>::iterator altIter = alternatives.begin(); + altIter != alternatives.end(); ++altIter) { + if (altIter != alternatives.begin()) { + altString << " , "; + } + altString << "" << m_OrganizerCore.directoryStructure()->getOriginByID(altIter->first).getName() << ""; + } + fileChild->setToolTip(1, QString("%1").arg(ToQString(altString.str()))); + fileChild->setForeground(1, QBrush(Qt::red)); + } else { + fileChild->setToolTip(1, tr("No conflict")); + } + subTree->addChild(fileChild); + } + } + + + //subTree->sortChildren(0, Qt::AscendingOrder); +} + +void MainWindow::delayedRemove() +{ + for (QTreeWidgetItem *item : m_RemoveWidget) { + item->removeChild(item->child(0)); + } + m_RemoveWidget.clear(); +} + +void MainWindow::expandDataTreeItem(QTreeWidgetItem *item) +{ + if ((item->childCount() == 1) && (item->child(0)->data(0, Qt::UserRole).toString() == "__loaded_on_demand__")) { + // read the data we need from the sub-item, then dispose of it + QTreeWidgetItem *onDemandDataItem = item->child(0); + std::wstring path = ToWString(onDemandDataItem->data(0, Qt::UserRole + 1).toString()); + bool conflictsOnly = onDemandDataItem->data(0, Qt::UserRole + 2).toBool(); + + std::wstring virtualPath = (path + L"\\").substr(6) + ToWString(item->text(0)); + DirectoryEntry *dir = m_OrganizerCore.directoryStructure()->findSubDirectoryRecursive(virtualPath); + if (dir != nullptr) { + QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); + QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); + updateTo(item, path, *dir, conflictsOnly, &fileIcon, &folderIcon); + } else { + qWarning("failed to update view of %ls", path.c_str()); + } + m_RemoveWidget.push_back(item); + QTimer::singleShot(5, this, SLOT(delayedRemove())); + } +} + + +bool MainWindow::refreshProfiles(bool selectProfile) +{ + QComboBox* profileBox = findChild("profileBox"); + + QString currentProfileName = profileBox->currentText(); + + profileBox->blockSignals(true); + profileBox->clear(); + profileBox->addItem(QObject::tr("")); + + QDir profilesDir(Settings::instance().getProfileDirectory()); + profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); + + QDirIterator profileIter(profilesDir); + + while (profileIter.hasNext()) { + profileIter.next(); + try { + profileBox->addItem(profileIter.fileName()); + } catch (const std::runtime_error& error) { + reportError(QObject::tr("failed to parse profile %1: %2").arg(profileIter.fileName()).arg(error.what())); + } + } + + // now select one of the profiles, preferably the one that was selected before + profileBox->blockSignals(false); + + if (selectProfile) { + if (profileBox->count() > 1) { + profileBox->setCurrentText(currentProfileName); + if (profileBox->currentIndex() == 0) { + profileBox->setCurrentIndex(1); + } + } + } + return profileBox->count() > 1; +} + + +void MainWindow::refreshExecutablesList() +{ + QComboBox* executablesList = findChild("executablesListBox"); + executablesList->setEnabled(false); + executablesList->clear(); + executablesList->addItem(tr("")); + + QAbstractItemModel *model = executablesList->model(); + + std::vector::const_iterator current, end; + m_OrganizerCore.executablesList()->getExecutables(current, end); + for(int i = 0; current != end; ++current, ++i) { + QIcon icon = iconForExecutable(current->m_BinaryInfo.filePath()); + executablesList->addItem(icon, current->m_Title); + model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole); + } + + setExecutableIndex(1); + executablesList->setEnabled(true); +} + + +void MainWindow::refreshDataTree() +{ + QCheckBox *conflictsBox = findChild("conflictsCheckBox"); + QTreeWidget *tree = findChild("dataTree"); + QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); + QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); + tree->clear(); + QStringList columns("data"); + columns.append(""); + QTreeWidgetItem *subTree = new QTreeWidgetItem(columns); + subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder)); + updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon); + tree->insertTopLevelItem(0, subTree); + subTree->setExpanded(true); +} + +void MainWindow::refreshDataTreeKeepExpandedNodes() +{ + QCheckBox *conflictsBox = findChild("conflictsCheckBox"); + QTreeWidget *tree = findChild("dataTree"); + QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder); + QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File); + QStringList expandedNodes; + QTreeWidgetItemIterator it1(tree, QTreeWidgetItemIterator::NotHidden | QTreeWidgetItemIterator::HasChildren); + while (*it1) { + QTreeWidgetItem *current = (*it1); + if (current->isExpanded() && !(current->text(0)=="data")) { + expandedNodes.append(current->text(0)+"/"+current->parent()->text(0)); + } + ++it1; + } + + tree->clear(); + QStringList columns("data"); + columns.append(""); + QTreeWidgetItem *subTree = new QTreeWidgetItem(columns); + subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder)); + updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon); + tree->insertTopLevelItem(0, subTree); + subTree->setExpanded(true); + QTreeWidgetItemIterator it2(tree, QTreeWidgetItemIterator::HasChildren); + while (*it2) { + QTreeWidgetItem *current = (*it2); + if (!(current->text(0)=="data") && expandedNodes.contains(current->text(0)+"/"+current->parent()->text(0))) { + current->setExpanded(true); + } + ++it2; + } +} + + +void MainWindow::refreshSavesIfOpen() +{ + if (ui->tabWidget->currentIndex() == 3) { + refreshSaveList(); + } +} + +QDir MainWindow::currentSavesDir() const +{ + QDir savesDir; + if (m_OrganizerCore.currentProfile()->localSavesEnabled()) { + savesDir.setPath(m_OrganizerCore.currentProfile()->savePath()); + } else { + QString iniPath = m_OrganizerCore.currentProfile()->localSettingsEnabled() + ? m_OrganizerCore.currentProfile()->absolutePath() + : m_OrganizerCore.managedGame()->documentsDirectory().absolutePath(); + iniPath += "/" + m_OrganizerCore.managedGame()->iniFiles()[0]; + + wchar_t path[MAX_PATH]; + ::GetPrivateProfileStringW( + L"General", L"SLocalSavePath", L"Saves", + path, MAX_PATH, + iniPath.toStdWString().c_str() + ); + savesDir.setPath(m_OrganizerCore.managedGame()->documentsDirectory().absoluteFilePath(QString::fromWCharArray(path))); + } + + return savesDir; +} + +void MainWindow::startMonitorSaves() +{ + stopMonitorSaves(); + + QDir savesDir = currentSavesDir(); + + m_SavesWatcher.addPath(savesDir.absolutePath()); +} + +void MainWindow::stopMonitorSaves() +{ + if (m_SavesWatcher.directories().length() > 0) { + m_SavesWatcher.removePaths(m_SavesWatcher.directories()); + } +} + +void MainWindow::refreshSaveList() +{ + ui->savegameList->clear(); + + startMonitorSaves(); // re-starts monitoring + + QStringList filters; + filters << QString("*.") + m_OrganizerCore.managedGame()->savegameExtension(); + + QDir savesDir = currentSavesDir(); + savesDir.setNameFilters(filters); + qDebug("reading save games from %s", qUtf8Printable(savesDir.absolutePath())); + + QFileInfoList files = savesDir.entryInfoList(QDir::Files, QDir::Time); + for (const QFileInfo &file : files) { + QListWidgetItem *item = new QListWidgetItem(file.fileName()); + item->setData(Qt::UserRole, file.absoluteFilePath()); + ui->savegameList->addItem(item); + } +} + + +static bool BySortValue(const std::pair &LHS, const std::pair &RHS) +{ + return LHS.first < RHS.first; +} + +template +static QStringList toStringList(InputIterator current, InputIterator end) +{ + QStringList result; + for (; current != end; ++current) { + result.append(*current); + } + return result; +} + +void MainWindow::updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives) +{ + m_DefaultArchives = defaultArchives; + ui->bsaList->clear(); + ui->bsaList->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + std::vector> items; + + BSAInvalidation * invalidation = m_OrganizerCore.managedGame()->feature(); + std::vector files = m_OrganizerCore.directoryStructure()->getFiles(); + + QStringList plugins = m_OrganizerCore.findFiles("", [](const QString &fileName) -> bool { + return fileName.endsWith(".esp", Qt::CaseInsensitive) + || fileName.endsWith(".esm", Qt::CaseInsensitive) + || fileName.endsWith(".esl", Qt::CaseInsensitive); + }); + + auto hasAssociatedPlugin = [&](const QString &bsaName) -> bool { + for (const QString &pluginName : plugins) { + QFileInfo pluginInfo(pluginName); + if (bsaName.startsWith(QFileInfo(pluginName).baseName(), Qt::CaseInsensitive) + && (m_OrganizerCore.pluginList()->state(pluginInfo.fileName()) == IPluginList::STATE_ACTIVE)) { + return true; + } + } + return false; + }; + + for (FileEntry::Ptr current : files) { + QFileInfo fileInfo(ToQString(current->getName().c_str())); + + if (fileInfo.suffix().toLower() == "bsa" || fileInfo.suffix().toLower() == "ba2") { + int index = activeArchives.indexOf(fileInfo.fileName()); + if (index == -1) { + index = 0xFFFF; + } + else { + index += 2; + } + + if ((invalidation != nullptr) && invalidation->isInvalidationBSA(fileInfo.fileName())) { + index = 1; + } + + int originId = current->getOrigin(); + FilesOrigin & origin = m_OrganizerCore.directoryStructure()->getOriginByID(originId); + + QTreeWidgetItem * newItem = new QTreeWidgetItem(QStringList() + << fileInfo.fileName() + << ToQString(origin.getName())); + newItem->setData(0, Qt::UserRole, index); + newItem->setData(1, Qt::UserRole, originId); + newItem->setFlags(newItem->flags() & ~(Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable)); + newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked); + newItem->setData(0, Qt::UserRole, false); + if (m_OrganizerCore.settings().forceEnableCoreFiles() + && defaultArchives.contains(fileInfo.fileName())) { + newItem->setCheckState(0, Qt::Checked); + newItem->setDisabled(true); + newItem->setData(0, Qt::UserRole, true); + } else if (fileInfo.fileName().compare("update.bsa", Qt::CaseInsensitive) == 0) { + newItem->setCheckState(0, Qt::Checked); + newItem->setDisabled(true); + } else if (hasAssociatedPlugin(fileInfo.fileName())) { + newItem->setCheckState(0, Qt::Checked); + newItem->setDisabled(true); + } else { + newItem->setCheckState(0, Qt::Unchecked); + newItem->setDisabled(true); + } + if (index < 0) index = 0; + + UINT32 sortValue = ((origin.getPriority() & 0xFFFF) << 16) | (index & 0xFFFF); + items.push_back(std::make_pair(sortValue, newItem)); + } + } + std::sort(items.begin(), items.end(), BySortValue); + + for (auto iter = items.begin(); iter != items.end(); ++iter) { + int originID = iter->second->data(1, Qt::UserRole).toInt(); + + FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID); + QString modName("data"); + unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName())); + if (modIndex != UINT_MAX) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + modName = modInfo->name(); + } + QList items = ui->bsaList->findItems(modName, Qt::MatchFixedString); + QTreeWidgetItem * subItem = nullptr; + if (items.length() > 0) { + subItem = items.at(0); + } + else { + subItem = new QTreeWidgetItem(QStringList(modName)); + subItem->setFlags(subItem->flags() & ~Qt::ItemIsDragEnabled); + ui->bsaList->addTopLevelItem(subItem); + } + subItem->addChild(iter->second); + subItem->setExpanded(true); + } + checkBSAList(); +} + +void MainWindow::checkBSAList() +{ + DataArchives * archives = m_OrganizerCore.managedGame()->feature(); + + if (archives != nullptr) { + ui->bsaList->blockSignals(true); + ON_BLOCK_EXIT([&]() { ui->bsaList->blockSignals(false); }); + + QStringList defaultArchives = archives->archives(m_OrganizerCore.currentProfile()); + + bool warning = false; + + for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { + bool modWarning = false; + QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i); + for (int j = 0; j < tlItem->childCount(); ++j) { + QTreeWidgetItem * item = tlItem->child(j); + QString filename = item->text(0); + item->setIcon(0, QIcon()); + item->setToolTip(0, QString()); + + if (item->checkState(0) == Qt::Unchecked) { + if (defaultArchives.contains(filename)) { + item->setIcon(0, QIcon(":/MO/gui/warning")); + item->setToolTip(0, tr("This bsa is enabled in the ini file so it may be required!")); + modWarning = true; + } + } + } + if (modWarning) { + ui->bsaList->expandItem(ui->bsaList->topLevelItem(i)); + warning = true; + } + } + if (warning) { + ui->tabWidget->setTabIcon(1, QIcon(":/MO/gui/warning")); + } else { + ui->tabWidget->setTabIcon(1, QIcon()); + } + } +} + +void MainWindow::saveModMetas() +{ + if (m_MetaSave.isFinished()) { + m_MetaSave = QtConcurrent::run([this]() { + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + modInfo->saveMeta(); + } + }); + } +} + +void MainWindow::fixCategories() +{ + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + std::set categories = modInfo->getCategories(); + for (std::set::iterator iter = categories.begin(); + iter != categories.end(); ++iter) { + if (!m_CategoryFactory.categoryExists(*iter)) { + modInfo->setCategory(*iter, false); + } + } + } +} + + +void MainWindow::setupNetworkProxy(bool activate) +{ + QNetworkProxyFactory::setUseSystemConfiguration(activate); +/* QNetworkProxyQuery query(QUrl("http://www.google.com"), QNetworkProxyQuery::UrlRequest); + query.setProtocolTag("http"); + QList proxies = QNetworkProxyFactory::systemProxyForQuery(query); + if ((proxies.size() > 0) && (proxies.at(0).type() != QNetworkProxy::NoProxy)) { + qDebug("Using proxy: %s", qUtf8Printable(proxies.at(0).hostName())); + QNetworkProxy::setApplicationProxy(proxies[0]); + } else { + qDebug("Not using proxy"); + }*/ +} + + +void MainWindow::activateProxy(bool activate) +{ + QProgressDialog busyDialog(tr("Activating Network Proxy"), QString(), 0, 0, parentWidget()); + busyDialog.setWindowFlags(busyDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); + busyDialog.setWindowModality(Qt::WindowModal); + busyDialog.show(); + QFuture future = QtConcurrent::run(MainWindow::setupNetworkProxy, activate); + while (!future.isFinished()) { + QCoreApplication::processEvents(); + ::Sleep(100); + } + busyDialog.hide(); +} + +void MainWindow::readSettings() +{ + QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat); + + if (settings.contains("window_geometry")) { + restoreGeometry(settings.value("window_geometry").toByteArray()); + } + + if (settings.contains("window_split")) { + ui->splitter->restoreState(settings.value("window_split").toByteArray()); + } + + if (settings.contains("log_split")) { + ui->topLevelSplitter->restoreState(settings.value("log_split").toByteArray()); + } + + bool filtersVisible = settings.value("filters_visible", false).toBool(); + setCategoryListVisible(filtersVisible); + ui->displayCategoriesBtn->setChecked(filtersVisible); + + int selectedExecutable = settings.value("selected_executable").toInt(); + setExecutableIndex(selectedExecutable); + + if (settings.value("Settings/use_proxy", false).toBool()) { + activateProxy(true); + } +} + +void MainWindow::processUpdates() { + QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat); + QVersionNumber lastVersion = QVersionNumber::fromString(settings.value("version", "2.1.2").toString()).normalized(); + QVersionNumber currentVersion = QVersionNumber::fromString(m_OrganizerCore.getVersion().displayString()).normalized(); + if (!m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) { + if (lastVersion < QVersionNumber(2, 1, 3)) { + bool lastHidden = true; + for (int i = ModList::COL_GAME; i < ui->modList->model()->columnCount(); ++i) { + bool hidden = ui->modList->header()->isSectionHidden(i); + ui->modList->header()->setSectionHidden(i, lastHidden); + lastHidden = hidden; + } + } + if (lastVersion < QVersionNumber(2,1,6)) { + ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true); + } + } + + if (currentVersion > lastVersion) { + //NOP + } else if (currentVersion < lastVersion) + qWarning() << tr("Notice: Your current MO version (%1) is lower than the previously used one (%2). " + "The GUI may not downgrade gracefully, so you may experience oddities. " + "However, there should be no serious issues.").arg(currentVersion.toString()).arg(lastVersion.toString()).toStdWString(); + //save version in all case + settings.setValue("version", currentVersion.toString()); +} + +void MainWindow::storeSettings(QSettings &settings) { + settings.setValue("group_state", ui->groupCombo->currentIndex()); + settings.setValue("selected_executable", + ui->executablesListBox->currentIndex()); + + if (settings.value("reset_geometry", false).toBool()) { + settings.remove("window_geometry"); + settings.remove("window_split"); + settings.remove("log_split"); + settings.remove("filters_visible"); + settings.remove("browser_geometry"); + settings.remove("geometry"); + settings.remove("reset_geometry"); + } else { + settings.setValue("window_geometry", saveGeometry()); + settings.setValue("window_split", ui->splitter->saveState()); + settings.setValue("log_split", ui->topLevelSplitter->saveState()); + settings.setValue("browser_geometry", m_IntegratedBrowser.saveGeometry()); + settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked()); + for (const std::pair kv : m_PersistedGeometry) { + QString key = QString("geometry/") + kv.first; + settings.setValue(key, kv.second->saveState()); + } + } +} + +ILockedWaitingForProcess* MainWindow::lock() +{ + if (m_LockDialog != nullptr) { + ++m_LockCount; + return m_LockDialog; + } + if (m_closing) + m_LockDialog = new WaitingOnCloseDialog(this); + else + m_LockDialog = new LockedDialog(this, true); + m_LockDialog->setModal(true); + m_LockDialog->show(); + setEnabled(false); + m_LockDialog->setEnabled(true); //What's the point otherwise? + ++m_LockCount; + return m_LockDialog; +} + +void MainWindow::unlock() +{ + //If you come through here with a null lock pointer, it's a bug! + if (m_LockDialog == nullptr) { + qDebug("Unlocking main window when already unlocked"); + return; + } + --m_LockCount; + if (m_LockCount == 0) { + if (m_closing && m_LockDialog->canceled()) + m_closing = false; + m_LockDialog->hide(); + m_LockDialog->deleteLater(); + m_LockDialog = nullptr; + setEnabled(true); + } +} + +void MainWindow::on_btnRefreshData_clicked() +{ + m_OrganizerCore.refreshDirectoryStructure(); +} + +void MainWindow::on_btnRefreshDownloads_clicked() +{ + m_OrganizerCore.downloadManager()->refreshList(); +} + +void MainWindow::on_tabWidget_currentChanged(int index) +{ + if (index == 0) { + m_OrganizerCore.refreshESPList(); + } else if (index == 1) { + m_OrganizerCore.refreshBSAList(); + } else if (index == 2) { + refreshDataTreeKeepExpandedNodes(); + } else if (index == 3) { + refreshSaveList(); + } +} + + +void MainWindow::installMod(QString fileName) +{ + try { + if (fileName.isEmpty()) { + QStringList extensions = m_OrganizerCore.installationManager()->getSupportedExtensions(); + for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) { + *iter = "*." + *iter; + } + + fileName = FileDialogMemory::getOpenFileName("installMod", this, tr("Choose Mod"), QString(), + tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" ")))); + } + + if (fileName.isEmpty()) { + return; + } else { + m_OrganizerCore.installMod(fileName, QString()); + } + } catch (const std::exception &e) { + reportError(e.what()); + } +} + +void MainWindow::on_startButton_clicked() { + ui->startButton->setEnabled(false); + try { + const Executable &selectedExecutable(getSelectedExecutable()); + QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.m_Title).toString(); + auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.m_Title); + if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.m_Title)) { + forcedLibraries.clear(); + } + m_OrganizerCore.spawnBinary( + selectedExecutable.m_BinaryInfo, selectedExecutable.m_Arguments, + selectedExecutable.m_WorkingDirectory.length() != 0 + ? selectedExecutable.m_WorkingDirectory + : selectedExecutable.m_BinaryInfo.absolutePath(), + selectedExecutable.m_SteamAppID, + customOverwrite, + forcedLibraries); + } catch (...) { + ui->startButton->setEnabled(true); + throw; + } + ui->startButton->setEnabled(true); +} + +static HRESULT CreateShortcut(LPCWSTR targetFileName, LPCWSTR arguments, + LPCSTR linkFileName, LPCWSTR description, + LPCTSTR iconFileName, int iconNumber, + LPCWSTR currentDirectory) +{ + HRESULT result = E_INVALIDARG; + if ((targetFileName != nullptr) && (wcslen(targetFileName) > 0) && + (arguments != nullptr) && + (linkFileName != nullptr) && (strlen(linkFileName) > 0) && + (description != nullptr) && + (currentDirectory != nullptr)) { + + IShellLink* shellLink; + result = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLink, (LPVOID*)&shellLink); + + if (!SUCCEEDED(result)) { + qCritical("failed to create IShellLink instance"); + return result; + } + + result = shellLink->SetPath(targetFileName); + if (!SUCCEEDED(result)) { + qCritical("failed to set target path %ls", targetFileName); + shellLink->Release(); + return result; + } + + result = shellLink->SetArguments(arguments); + if (!SUCCEEDED(result)) { + qCritical("failed to set arguments: %ls", arguments); + shellLink->Release(); + return result; + } + + if (wcslen(description) > 0) { + result = shellLink->SetDescription(description); + if (!SUCCEEDED(result)) { + qCritical("failed to set description: %ls", description); + shellLink->Release(); + return result; + } + } + + if (wcslen(currentDirectory) > 0) { + result = shellLink->SetWorkingDirectory(currentDirectory); + if (!SUCCEEDED(result)) { + qCritical("failed to set working directory: %ls", currentDirectory); + shellLink->Release(); + return result; + } + } + + if (iconFileName != nullptr) { + result = shellLink->SetIconLocation(iconFileName, iconNumber); + if (!SUCCEEDED(result)) { + qCritical("failed to load program icon: %ls %d", iconFileName, iconNumber); + shellLink->Release(); + return result; + } + } + + IPersistFile *persistFile; + result = shellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); + if (SUCCEEDED(result)) { + wchar_t linkFileNameW[MAX_PATH]; + if (MultiByteToWideChar(CP_ACP, 0, linkFileName, -1, linkFileNameW, MAX_PATH) > 0) { + result = persistFile->Save(linkFileNameW, TRUE); + } else { + qCritical("failed to create link: %s", linkFileName); + } + persistFile->Release(); + } else { + qCritical("failed to create IPersistFile instance"); + } + + shellLink->Release(); + } + return result; +} + + +bool MainWindow::modifyExecutablesDialog() +{ + bool result = false; + try { + EditExecutablesDialog dialog(*m_OrganizerCore.executablesList(), + *m_OrganizerCore.modList(), + m_OrganizerCore.currentProfile(), + m_OrganizerCore.managedGame()); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog.objectName()); + if (settings.contains(key)) { + dialog.restoreGeometry(settings.value(key).toByteArray()); + } + if (dialog.exec() == QDialog::Accepted) { + m_OrganizerCore.setExecutablesList(dialog.getExecutablesList()); + result = true; + } + settings.setValue(key, dialog.saveGeometry()); + refreshExecutablesList(); + } catch (const std::exception &e) { + reportError(e.what()); + } + return result; +} + +void MainWindow::on_executablesListBox_currentIndexChanged(int index) +{ + QComboBox* executablesList = findChild("executablesListBox"); + + int previousIndex = m_OldExecutableIndex; + m_OldExecutableIndex = index; + + if (executablesList->isEnabled()) { + //I think the 2nd test is impossible + if ((index == 0) || (index > static_cast(m_OrganizerCore.executablesList()->size()))) { + if (modifyExecutablesDialog()) { + setExecutableIndex(previousIndex); + } + } else { + setExecutableIndex(index); + } + } +} + +void MainWindow::helpTriggered() +{ + QWhatsThis::enterWhatsThisMode(); +} + +void MainWindow::wikiTriggered() +{ + QDesktopServices::openUrl(QUrl("https://modorganizer2.github.io/")); +} + +void MainWindow::discordTriggered() +{ + QDesktopServices::openUrl(QUrl("https://discord.gg/cYwdcxj")); +} + +void MainWindow::issueTriggered() +{ + QDesktopServices::openUrl(QUrl("https://github.com/Modorganizer2/modorganizer/issues")); +} + +void MainWindow::tutorialTriggered() +{ + QAction *tutorialAction = qobject_cast(sender()); + if (tutorialAction != nullptr) { + if (QMessageBox::question(this, tr("Start Tutorial?"), + tr("You're about to start a tutorial. For technical reasons it's not possible to end " + "the tutorial early. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + TutorialManager::instance().activateTutorial("MainWindow", tutorialAction->data().toString()); + } + } +} + + +void MainWindow::on_actionInstallMod_triggered() +{ + installMod(); +} + +void MainWindow::on_actionAdd_Profile_triggered() +{ + for (;;) { + ProfilesDialog profilesDialog(m_OrganizerCore.currentProfile()->name(), + m_OrganizerCore.managedGame(), + this); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(profilesDialog.objectName()); + if (settings.contains(key)) { + profilesDialog.restoreGeometry(settings.value(key).toByteArray()); + } + // workaround: need to disable monitoring of the saves directory, otherwise the active + // profile directory is locked + stopMonitorSaves(); + profilesDialog.exec(); + settings.setValue(key, profilesDialog.saveGeometry()); + refreshSaveList(); // since the save list may now be outdated we have to refresh it completely + if (refreshProfiles() && !profilesDialog.failed()) { + break; + } + } + + LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature(); + if (saveGames != nullptr) { + if (saveGames->prepareProfile(m_OrganizerCore.currentProfile())) + refreshSaveList(); + } + + BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature(); + if (invalidation != nullptr) { + if (invalidation->prepareProfile(m_OrganizerCore.currentProfile())) + QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh())); + } +} + +void MainWindow::on_actionModify_Executables_triggered() +{ + if (modifyExecutablesDialog()) { + setExecutableIndex(m_OldExecutableIndex); + } +} + + +void MainWindow::setModListSorting(int index) +{ + Qt::SortOrder order = ((index & 0x01) != 0) ? Qt::DescendingOrder : Qt::AscendingOrder; + int column = index >> 1; + ui->modList->header()->setSortIndicator(column, order); +} + + +void MainWindow::setESPListSorting(int index) +{ + switch (index) { + case 0: { + ui->espList->header()->setSortIndicator(1, Qt::AscendingOrder); + } break; + case 1: { + ui->espList->header()->setSortIndicator(1, Qt::DescendingOrder); + } break; + case 2: { + ui->espList->header()->setSortIndicator(0, Qt::AscendingOrder); + } break; + case 3: { + ui->espList->header()->setSortIndicator(0, Qt::DescendingOrder); + } break; + } +} + +void MainWindow::refresher_progress(int percent) +{ + if (percent == 100) { + m_RefreshProgress->setVisible(false); + statusBar()->hide(); + this->setEnabled(true); + } else if (!m_RefreshProgress->isVisible()) { + this->setEnabled(false); + statusBar()->show(); + m_RefreshProgress->setVisible(true); + m_RefreshProgress->setRange(0, 100); + m_RefreshProgress->setValue(percent); + } +} + +void MainWindow::directory_refreshed() +{ + // some problem-reports may rely on the virtual directory tree so they need to be updated + // now + refreshDataTreeKeepExpandedNodes(); + updateProblemsButton(); + statusBar()->hide(); +} + +void MainWindow::esplist_changed() +{ + updatePluginCount(); +} + +void MainWindow::modorder_changed() +{ + for (unsigned int i = 0; i < m_OrganizerCore.currentProfile()->numMods(); ++i) { + int priority = m_OrganizerCore.currentProfile()->getModPriority(i); + if (m_OrganizerCore.currentProfile()->modEnabled(i)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + // priorities in the directory structure are one higher because data is 0 + m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->internalName())).setPriority(priority + 1); + } + } + m_OrganizerCore.refreshBSAList(); + m_OrganizerCore.currentProfile()->writeModlist(); + m_ArchiveListWriter.write(); + m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins(); + + { // refresh selection + QModelIndex current = ui->modList->currentIndex(); + if (current.isValid()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt()); + // clear caches on all mods conflicting with the moved mod + for (int i : modInfo->getModOverwrite()) { + ModInfo::getByIndex(i)->clearCaches(); + } + for (int i : modInfo->getModOverwritten()) { + ModInfo::getByIndex(i)->clearCaches(); + } + for (int i : modInfo->getModArchiveOverwrite()) { + ModInfo::getByIndex(i)->clearCaches(); + } + for (int i : modInfo->getModArchiveOverwritten()) { + ModInfo::getByIndex(i)->clearCaches(); + } + for (int i : modInfo->getModArchiveLooseOverwrite()) { + ModInfo::getByIndex(i)->clearCaches(); + } + for (int i : modInfo->getModArchiveLooseOverwritten()) { + ModInfo::getByIndex(i)->clearCaches(); + } + // update conflict check on the moved mod + modInfo->doConflictCheck(); + m_OrganizerCore.modList()->setOverwriteMarkers(modInfo->getModOverwrite(), modInfo->getModOverwritten()); + m_OrganizerCore.modList()->setArchiveOverwriteMarkers(modInfo->getModArchiveOverwrite(), modInfo->getModArchiveOverwritten()); + m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(modInfo->getModArchiveLooseOverwrite(), modInfo->getModArchiveLooseOverwritten()); + if (m_ModListSortProxy != nullptr) { + m_ModListSortProxy->invalidate(); + } + ui->modList->verticalScrollBar()->repaint(); + } + } +} + +void MainWindow::modInstalled(const QString &modName) +{ + QModelIndexList posList = + m_OrganizerCore.modList()->match(m_OrganizerCore.modList()->index(0, 0), Qt::DisplayRole, modName); + if (posList.count() == 1) { + ui->modList->scrollTo(posList.at(0)); + } +} + +void MainWindow::procError(QProcess::ProcessError error) +{ + reportError(tr("failed to spawn notepad.exe: %1").arg(error)); + this->sender()->deleteLater(); +} + +void MainWindow::procFinished(int, QProcess::ExitStatus) +{ + this->sender()->deleteLater(); +} + +void MainWindow::showMessage(const QString &message) +{ + MessageDialog::showMessage(message, this); +} + +void MainWindow::showError(const QString &message) +{ + reportError(message); +} + +void MainWindow::installMod_clicked() +{ + installMod(); +} + +void MainWindow::modRenamed(const QString &oldName, const QString &newName) +{ + Profile::renameModInAllProfiles(oldName, newName); + + // immediately refresh the active profile because the data in memory is invalid + m_OrganizerCore.currentProfile()->refreshModStatus(); + + // also fix the directory structure + try { + if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldName))) { + FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldName)); + origin.setName(ToWString(newName)); + } else { + + } + } catch (const std::exception &e) { + reportError(tr("failed to change origin name: %1").arg(e.what())); + } +} + +void MainWindow::fileMoved(const QString &filePath, const QString &oldOriginName, const QString &newOriginName) +{ + const FileEntry::Ptr filePtr = m_OrganizerCore.directoryStructure()->findFile(ToWString(filePath)); + if (filePtr.get() != nullptr) { + try { + if (m_OrganizerCore.directoryStructure()->originExists(ToWString(newOriginName))) { + FilesOrigin &newOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(newOriginName)); + + QString fullNewPath = ToQString(newOrigin.getPath()) + "\\" + filePath; + WIN32_FIND_DATAW findData; + HANDLE hFind; + hFind = ::FindFirstFileW(ToWString(fullNewPath).c_str(), &findData); + filePtr->addOrigin(newOrigin.getID(), findData.ftCreationTime, L"", -1); + FindClose(hFind); + } + if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldOriginName))) { + FilesOrigin &oldOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldOriginName)); + filePtr->removeOrigin(oldOrigin.getID()); + } + } catch (const std::exception &e) { + reportError(tr("failed to move \"%1\" from mod \"%2\" to \"%3\": %4").arg(filePath).arg(oldOriginName).arg(newOriginName).arg(e.what())); + } + } else { + // this is probably not an error, the specified path is likely a directory + } +} + +QTreeWidgetItem *MainWindow::addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type) +{ + QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name)); + item->setData(0, Qt::ToolTipRole, name); + item->setData(0, Qt::UserRole, categoryID); + item->setData(0, Qt::UserRole + 1, type); + if (root != nullptr) { + root->addChild(item); + } else { + ui->categoriesList->addTopLevelItem(item); + } + return item; +} + +void MainWindow::addContentFilters() +{ + for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) { + addFilterItem(nullptr, tr("").arg(ModInfo::getContentTypeName(i)), i, ModListSortProxy::TYPE_CONTENT); + } +} + +void MainWindow::addCategoryFilters(QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID) +{ + for (unsigned int i = 1; + i < static_cast(m_CategoryFactory.numCategories()); ++i) { + if ((m_CategoryFactory.getParentID(i) == targetID)) { + int categoryID = m_CategoryFactory.getCategoryID(i); + if (categoriesUsed.find(categoryID) != categoriesUsed.end()) { + QTreeWidgetItem *item = + addFilterItem(root, m_CategoryFactory.getCategoryName(i), + categoryID, ModListSortProxy::TYPE_CATEGORY); + if (m_CategoryFactory.hasChildren(i)) { + addCategoryFilters(item, categoriesUsed, categoryID); + } + } + } + } +} + +void MainWindow::refreshFilters() +{ + QItemSelection currentSelection = ui->modList->selectionModel()->selection(); + + QVariant currentIndexName = ui->modList->currentIndex().data(); + ui->modList->setCurrentIndex(QModelIndex()); + + QStringList selectedItems; + for (QTreeWidgetItem *item : ui->categoriesList->selectedItems()) { + selectedItems.append(item->text(0)); + } + + ui->categoriesList->clear(); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CHECKED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_BACKUP, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_MANAGED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNMANAGED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CONFLICT, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED, ModListSortProxy::TYPE_SPECIAL); + + addContentFilters(); + std::set categoriesUsed; + for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx); + for (int categoryID : modInfo->getCategories()) { + int currentID = categoryID; + std::set cycleTest; + // also add parents so they show up in the tree + while (currentID != 0) { + categoriesUsed.insert(currentID); + if (!cycleTest.insert(currentID).second) { + qWarning("cycle in categories: %s", qUtf8Printable(SetJoin(cycleTest, ", "))); + break; + } + currentID = m_CategoryFactory.getParentID(m_CategoryFactory.getCategoryIndex(currentID)); + } + } + } + + addCategoryFilters(nullptr, categoriesUsed, 0); + + for (const QString &item : selectedItems) { + QList matches = ui->categoriesList->findItems(item, Qt::MatchFixedString | Qt::MatchRecursive); + if (matches.size() > 0) { + matches.at(0)->setSelected(true); + } + } + ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select); + QModelIndexList matchList; + if (currentIndexName.isValid()) { + matchList = ui->modList->model()->match(ui->modList->model()->index(0, 0), Qt::DisplayRole, currentIndexName); + } + + if (matchList.size() > 0) { + ui->modList->setCurrentIndex(matchList.at(0)); + } +} + + +void MainWindow::renameMod_clicked() +{ + try { + ui->modList->edit(ui->modList->currentIndex()); + } catch (const std::exception &e) { + reportError(tr("failed to rename mod: %1").arg(e.what())); + } +} + + +void MainWindow::restoreBackup_clicked() +{ + QRegExp backupRegEx("(.*)_backup[0-9]*$"); + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + if (backupRegEx.indexIn(modInfo->name()) != -1) { + QString regName = backupRegEx.cap(1); + QDir modDir(QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory())); + if (!modDir.exists(regName) || + (QMessageBox::question(this, tr("Overwrite?"), + tr("This will replace the existing mod \"%1\". Continue?").arg(regName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { + if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) { + reportError(tr("failed to remove mod \"%1\"").arg(regName)); + } else { + QString destinationPath = QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()) + "/" + regName; + if (!modDir.rename(modInfo->absolutePath(), destinationPath)) { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath)); + } + m_OrganizerCore.refreshModList(); + } + } + } +} + +void MainWindow::modlistChanged(const QModelIndex&, int) +{ + m_OrganizerCore.currentProfile()->writeModlist(); + updateModCount(); +} + +void MainWindow::modlistChanged(const QModelIndexList&, int) +{ + m_OrganizerCore.currentProfile()->writeModlist(); + updateModCount(); +} + +void MainWindow::modlistSelectionChanged(const QModelIndex ¤t, const QModelIndex&) +{ + if (current.isValid()) { + ModInfo::Ptr selectedMod = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt()); + m_OrganizerCore.modList()->setOverwriteMarkers(selectedMod->getModOverwrite(), selectedMod->getModOverwritten()); + m_OrganizerCore.modList()->setArchiveOverwriteMarkers(selectedMod->getModArchiveOverwrite(), selectedMod->getModArchiveOverwritten()); + m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(selectedMod->getModArchiveLooseOverwrite(), selectedMod->getModArchiveLooseOverwritten()); + } else { + m_OrganizerCore.modList()->setOverwriteMarkers(std::set(), std::set()); + m_OrganizerCore.modList()->setArchiveOverwriteMarkers(std::set(), std::set()); + m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(std::set(), std::set()); + } +/* if ((m_ModListSortProxy != nullptr) + && !m_ModListSortProxy->beingInvalidated()) { + m_ModListSortProxy->invalidate(); + }*/ + ui->modList->verticalScrollBar()->repaint(); +} + +void MainWindow::modlistSelectionsChanged(const QItemSelection &selected) +{ + m_OrganizerCore.pluginList()->highlightPlugins(ui->modList->selectionModel(), *m_OrganizerCore.directoryStructure(), *m_OrganizerCore.currentProfile()); + ui->espList->verticalScrollBar()->repaint(); +} + +void MainWindow::esplistSelectionsChanged(const QItemSelection &selected) +{ + m_OrganizerCore.modList()->highlightMods(ui->espList->selectionModel(), *m_OrganizerCore.directoryStructure()); + ui->modList->verticalScrollBar()->repaint(); +} + +void MainWindow::modListSortIndicatorChanged(int, Qt::SortOrder) +{ + ui->modList->verticalScrollBar()->repaint(); +} + +void MainWindow::modListSectionResized(int logicalIndex, int oldSize, int newSize) +{ + bool enabled = (newSize != 0); + qobject_cast(ui->modList->model())->setColumnVisible(logicalIndex, enabled); +} + +void MainWindow::removeMod_clicked() +{ + try { + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + QString mods; + QStringList modNames; + for (QModelIndex idx : selection->selectedRows()) { + QString name = idx.data().toString(); + if (!ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->isRegular()) { + continue; + } + mods += "
  • " + name + "
  • "; + modNames.append(ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->name()); + } + if (QMessageBox::question(this, tr("Confirm"), + tr("Remove the following mods?
      %1
    ").arg(mods), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + // use mod names instead of indexes because those become invalid during the removal + DownloadManager::startDisableDirWatcher(); + for (QString name : modNames) { + m_OrganizerCore.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex()); + } + DownloadManager::endDisableDirWatcher(); + } + } else { + m_OrganizerCore.modList()->removeRow(m_ContextRow, QModelIndex()); + } + updateModCount(); + updatePluginCount(); + } catch (const std::exception &e) { + reportError(tr("failed to remove mod: %1").arg(e.what())); + } +} + + +void MainWindow::modRemoved(const QString &fileName) +{ + if (!fileName.isEmpty() && !QFileInfo(fileName).isAbsolute()) { + m_OrganizerCore.downloadManager()->markUninstalled(fileName); + } +} + + +void MainWindow::reinstallMod_clicked() +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + QString installationFile = modInfo->getInstallationFile(); + if (installationFile.length() != 0) { + QString fullInstallationFile; + QFileInfo fileInfo(installationFile); + if (fileInfo.isAbsolute()) { + if (fileInfo.exists()) { + fullInstallationFile = installationFile; + } else { + fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + fileInfo.fileName(); + } + } else { + fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + installationFile; + } + if (QFile::exists(fullInstallationFile)) { + m_OrganizerCore.installMod(fullInstallationFile, modInfo->name()); + } else { + QMessageBox::information(this, tr("Failed"), tr("Installation file no longer exists")); + } + } else { + QMessageBox::information(this, tr("Failed"), + tr("Mods installed with old versions of MO can't be reinstalled in this way.")); + } +} + +void MainWindow::backupMod_clicked() +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + QString backupDirectory = m_OrganizerCore.installationManager()->generateBackupName(modInfo->absolutePath()); + if (!copyDir(modInfo->absolutePath(), backupDirectory, false)) { + QMessageBox::information(this, tr("Failed"), + tr("Failed to create backup.")); + } + m_OrganizerCore.refreshModList(); +} + +void MainWindow::resumeDownload(int downloadIndex) +{ + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + m_OrganizerCore.downloadManager()->resumeDownload(downloadIndex); + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + m_OrganizerCore.doAfterLogin([this, downloadIndex] () { + this->resumeDownload(downloadIndex); + }); + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to resume a download"), this); + } + } +} + + +void MainWindow::endorseMod(ModInfo::Ptr mod) +{ + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + mod->endorse(true); + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + m_OrganizerCore.doAfterLogin(boost::bind(&MainWindow::endorseMod, this, mod)); + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); + } + } +} + + +void MainWindow::endorse_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { + MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), this); + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(true); + } + } + else { + QString username, password; + if (m_OrganizerCore.settings().getNexusLogin(username, password)) { + MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), this); + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + m_OrganizerCore.doAfterLogin(boost::bind(&MainWindow::endorseMod, this, modInfo)); + } + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); + return; + } + } + } + else { + endorseMod(ModInfo::getByIndex(m_ContextRow)); + } +} + +void MainWindow::dontendorse_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->setNeverEndorse(); + } + } + else { + ModInfo::getByIndex(m_ContextRow)->setNeverEndorse(); + } +} + + +void MainWindow::unendorseMod(ModInfo::Ptr mod) +{ + QString apiKey; + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + ModInfo::getByIndex(m_ContextRow)->endorse(false); + } else { + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + m_OrganizerCore.doAfterLogin([this] () { this->unendorse_clicked(); }); + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); + } + } +} + + +void MainWindow::unendorse_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) { + MessageDialog::showMessage(tr("Unendorsing multiple mods will take a while. Please wait..."), this); + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(false); + } + } + else { + QString username, password; + if (m_OrganizerCore.settings().getNexusLogin(username, password)) { + MessageDialog::showMessage(tr("Unendorsing multiple mods will take a while. Please wait..."), this); + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + m_OrganizerCore.doAfterLogin(boost::bind(&MainWindow::unendorseMod, this, modInfo)); + } + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); + return; + } + } + } + else { + unendorseMod(ModInfo::getByIndex(m_ContextRow)); + } +} + +void MainWindow::loginFailed(const QString &error) +{ + qDebug("login failed: %s", qUtf8Printable(error)); + statusBar()->hide(); +} + +void MainWindow::windowTutorialFinished(const QString &windowName) +{ + m_OrganizerCore.settings().directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true); +} + +void MainWindow::overwriteClosed(int) +{ + OverwriteInfoDialog *dialog = this->findChild("__overwriteDialog"); + if (dialog != nullptr) { + m_OrganizerCore.modList()->modInfoChanged(dialog->modInfo()); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog->objectName()); + settings.setValue(key, dialog->saveGeometry()); + dialog->deleteLater(); + } + m_OrganizerCore.refreshDirectoryStructure(); +} + + +void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab) +{ + if (!m_OrganizerCore.modList()->modInfoAboutToChange(modInfo)) { + qDebug("A different mod information dialog is open. If this is incorrect, please restart MO"); + return; + } + std::vector flags = modInfo->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { + QDialog *dialog = this->findChild("__overwriteDialog"); + try { + if (dialog == nullptr) { + dialog = new OverwriteInfoDialog(modInfo, this); + dialog->setObjectName("__overwriteDialog"); + } else { + qobject_cast(dialog)->setModInfo(modInfo); + } + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog->objectName()); + if (settings.contains(key)) { + dialog->restoreGeometry(settings.value(key).toByteArray()); + } + dialog->show(); + dialog->raise(); + dialog->activateWindow(); + connect(dialog, SIGNAL(finished(int)), this, SLOT(overwriteClosed(int))); + } catch (const std::exception &e) { + reportError(tr("Failed to display overwrite dialog: %1").arg(e.what())); + } + } else { + modInfo->saveMeta(); + ModInfoDialog dialog(modInfo, m_OrganizerCore.directoryStructure(), modInfo->hasFlag(ModInfo::FLAG_FOREIGN), &m_OrganizerCore, &m_PluginContainer, this); + connect(&dialog, SIGNAL(linkActivated(QString)), this, SLOT(linkClicked(QString))); + connect(&dialog, SIGNAL(downloadRequest(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); + connect(&dialog, SIGNAL(modOpen(QString, int)), this, SLOT(displayModInformation(QString, int)), Qt::QueuedConnection); + connect(&dialog, SIGNAL(modOpenNext(int)), this, SLOT(modOpenNext(int)), Qt::QueuedConnection); + connect(&dialog, SIGNAL(modOpenPrev(int)), this, SLOT(modOpenPrev(int)), Qt::QueuedConnection); + connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); + connect(&dialog, SIGNAL(endorseMod(ModInfo::Ptr)), this, SLOT(endorseMod(ModInfo::Ptr))); + + //Open the tab first if we want to use the standard indexes of the tabs. + if (tab != -1) { + dialog.openTab(tab); + } + + dialog.restoreTabState(m_OrganizerCore.settings().directInterface().value("mod_info_tabs").toByteArray()); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog.objectName()); + if (settings.contains(key)) { + dialog.restoreGeometry(settings.value(key).toByteArray()); + } + + //If no tab was specified use the first tab from the left based on the user order. + if (tab == -1) { + for (int i = 0; i < dialog.findChild("tabWidget")->count(); ++i) { + if (dialog.findChild("tabWidget")->isTabEnabled(i)) { + dialog.findChild("tabWidget")->setCurrentIndex(i); + break; + } + } + } + + dialog.exec(); + m_OrganizerCore.settings().directInterface().setValue("mod_info_tabs", dialog.saveTabState()); + settings.setValue(key, dialog.saveGeometry()); + + modInfo->saveMeta(); + emit modInfoDisplayed(); + m_OrganizerCore.modList()->modInfoChanged(modInfo); + } + + if (m_OrganizerCore.currentProfile()->modEnabled(index) + && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { + FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + + if (m_OrganizerCore.directoryStructure()->originExists(ToWString(modInfo->name()))) { + FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + + m_OrganizerCore.directoryRefresher()->addModToStructure(m_OrganizerCore.directoryStructure() + , modInfo->name() + , m_OrganizerCore.currentProfile()->getModPriority(index) + , modInfo->absolutePath() + , modInfo->stealFiles() + , modInfo->archives()); + DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure()); + m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins(); + m_OrganizerCore.refreshLists(); + } + } +} + +bool MainWindow::closeWindow() +{ + return close(); +} + +void MainWindow::setWindowEnabled(bool enabled) +{ + setEnabled(enabled); +} + + +void MainWindow::modOpenNext(int tab) +{ + QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0)); + index = m_ModListSortProxy->index((index.row() + 1) % m_ModListSortProxy->rowCount(), 0); + + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); + std::vector flags = mod->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) || + (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) || + (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())) { + // skip overwrite and backups and separators + modOpenNext(tab); + } else { + displayModInformation(m_ContextRow,tab); + } +} + +void MainWindow::modOpenPrev(int tab) +{ + QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0)); + int row = index.row() - 1; + if (row == -1) { + row = m_ModListSortProxy->rowCount() - 1; + } + + m_ContextRow = m_ModListSortProxy->mapToSource(m_ModListSortProxy->index(row, 0)).row(); + ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); + std::vector flags = mod->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) || + (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) || + (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())) { + // skip overwrite and backups and separators + modOpenPrev(tab); + } else { + displayModInformation(m_ContextRow,tab); + } +} + +void MainWindow::displayModInformation(const QString &modName, int tab) +{ + unsigned int index = ModInfo::getIndex(modName); + if (index == UINT_MAX) { + qCritical("failed to resolve mod name %s", qUtf8Printable(modName)); + return; + } + + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + displayModInformation(modInfo, index, tab); +} + + +void MainWindow::displayModInformation(int row, int tab) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + displayModInformation(modInfo, row, tab); +} + + +void MainWindow::ignoreMissingData_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + int row_idx = idx.data(Qt::UserRole + 1).toInt(); + ModInfo::Ptr info = ModInfo::getByIndex(row_idx); + //QDir(info->absolutePath()).mkdir("textures"); + info->testValid(); + info->markValidated(true); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); + + emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1)); + } + } else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + //QDir(info->absolutePath()).mkdir("textures"); + info->testValid(); + info->markValidated(true); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); + + emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); + } +} + +void MainWindow::markConverted_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + int row_idx = idx.data(Qt::UserRole + 1).toInt(); + ModInfo::Ptr info = ModInfo::getByIndex(row_idx); + info->markConverted(true); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); + emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1)); + } + } else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->markConverted(true); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); + emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); + } +} + + +void MainWindow::visitOnNexus_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + int count = selection->selectedRows().count(); + if (count > 10) { + if (QMessageBox::question(this, tr("Opening Nexus Links"), + tr("You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?").arg(count), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + int row_idx; + ModInfo::Ptr info; + QString gameName; + QString webUrl; + for (QModelIndex idx : selection->selectedRows()) { + row_idx = idx.data(Qt::UserRole + 1).toInt(); + info = ModInfo::getByIndex(row_idx); + int modID = info->getNexusID(); + webUrl = info->getURL(); + gameName = info->getGameName(); + if (modID > 0) { + linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); + } + else if (webUrl != "") { + linkClicked(webUrl); + } + } + } + else { + int modID = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole).toInt(); + QString gameName = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole + 4).toString(); + if (modID > 0) { + linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); + } else { + MessageDialog::showMessage(tr("Nexus ID for this Mod is unknown"), this); + } + } +} + +void MainWindow::visitWebPage_clicked() +{ + + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + int count = selection->selectedRows().count(); + if (count > 10) { + if (QMessageBox::question(this, tr("Opening Web Pages"), + tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(count), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + int row_idx; + ModInfo::Ptr info; + QString gameName; + QString webUrl; + for (QModelIndex idx : selection->selectedRows()) { + row_idx = idx.data(Qt::UserRole + 1).toInt(); + info = ModInfo::getByIndex(row_idx); + int modID = info->getNexusID(); + webUrl = info->getURL(); + gameName = info->getGameName(); + if (modID > 0) { + linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); + } + else if (webUrl != "") { + linkClicked(webUrl); + } + } + } + else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + if (info->getURL() != "") { + linkClicked(info->getURL()); + } + else { + MessageDialog::showMessage(tr("Web page for this mod is unknown"), this); + } + } +} + +void MainWindow::openExplorer_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + ::ShellExecuteW(nullptr, L"explore", ToWString(info->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + } + else { + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } +} + +void MainWindow::openOriginExplorer_clicked() +{ + QItemSelectionModel *selection = ui->espList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 0) { + for (QModelIndex idx : selection->selectedRows()) { + QString fileName = idx.data().toString(); + unsigned int modIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)); + if (modIndex == UINT_MAX) { + continue; + } + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + std::vector flags = modInfo->getFlags(); + + ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + } + else { + QModelIndex idx = selection->currentIndex(); + QString fileName = idx.data().toString(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector flags = modInfo->getFlags(); + + ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } +} + +void MainWindow::openExplorer_activated() +{ + if (ui->modList->hasFocus()) { + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() == 1 ) { + + QModelIndex idx = selection->currentIndex(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + std::vector flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + + } + } + + if (ui->espList->hasFocus()) { + QItemSelectionModel *selection = ui->espList->selectionModel(); + + if (selection->hasSelection() && selection->selectedRows().count() == 1) { + + QModelIndex idx = selection->currentIndex(); + QString fileName = idx.data().toString(); + + + unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)); + if (modInfoIndex != UINT_MAX) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); + std::vector flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + } + } + } +} + +void MainWindow::refreshProfile_activated() +{ + m_OrganizerCore.profileRefresh(); +} + +void MainWindow::search_activated() +{ + if (ui->modList->hasFocus() || ui->modFilterEdit->hasFocus()) { + ui->modFilterEdit->setFocus(); + ui->modFilterEdit->setSelection(0, INT_MAX); + } + + else if (ui->espList->hasFocus() || ui->espFilterEdit->hasFocus()) { + ui->espFilterEdit->setFocus(); + ui->espFilterEdit->setSelection(0, INT_MAX); + } + + else if (ui->downloadView->hasFocus() || ui->downloadFilterEdit->hasFocus()) { + ui->downloadFilterEdit->setFocus(); + ui->downloadFilterEdit->setSelection(0, INT_MAX); + } +} + +void MainWindow::searchClear_activated() +{ + if (ui->modList->hasFocus() || ui->modFilterEdit->hasFocus()) { + ui->modFilterEdit->clear(); + ui->modList->setFocus(); + } + + else if (ui->espList->hasFocus() || ui->espFilterEdit->hasFocus()) { + ui->espFilterEdit->clear(); + ui->espList->setFocus(); + } + + else if (ui->downloadView->hasFocus() || ui->downloadFilterEdit->hasFocus()) { + ui->downloadFilterEdit->clear(); + ui->downloadView->setFocus(); + } +} + +void MainWindow::updateModCount() +{ + int activeCount = 0; + int visActiveCount = 0; + int backupCount = 0; + int visBackupCount = 0; + int foreignCount = 0; + int visForeignCount = 0; + int separatorCount = 0; + int visSeparatorCount = 0; + int regularCount = 0; + int visRegularCount = 0; + + QStringList allMods = m_OrganizerCore.modList()->allMods(); + + auto hasFlag = [](std::vector flags, ModInfo::EFlag filter) { + return std::find(flags.begin(), flags.end(), filter) != flags.end(); + }; + + bool isEnabled; + bool isVisible; + for (QString mod : allMods) { + int modIndex = ModInfo::getIndex(mod); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + std::vector modFlags = modInfo->getFlags(); + isEnabled = m_OrganizerCore.currentProfile()->modEnabled(modIndex); + isVisible = m_ModListSortProxy->filterMatchesMod(modInfo, isEnabled); + + for (auto flag : modFlags) { + switch (flag) { + case ModInfo::FLAG_BACKUP: backupCount++; + if (isVisible) + visBackupCount++; + break; + case ModInfo::FLAG_FOREIGN: foreignCount++; + if (isVisible) + visForeignCount++; + break; + case ModInfo::FLAG_SEPARATOR: separatorCount++; + if (isVisible) + visSeparatorCount++; + break; + } + } + + if (!hasFlag(modFlags, ModInfo::FLAG_BACKUP) && + !hasFlag(modFlags, ModInfo::FLAG_FOREIGN) && + !hasFlag(modFlags, ModInfo::FLAG_SEPARATOR) && + !hasFlag(modFlags, ModInfo::FLAG_OVERWRITE)) { + if (isEnabled) { + activeCount++; + if (isVisible) + visActiveCount++; + } + if (isVisible) + visRegularCount++; + regularCount++; + } + } + + ui->activeModsCounter->display(visActiveCount); + ui->activeModsCounter->setToolTip(tr("" + "" + "" + "" + "" + "" + "
    TypeAllVisible
    Enabled mods: %1 / %2%3 / %4
    Unmanaged/DLCs: %5%6
    Mod backups: %7%8
    Separators: %9%10
    ") + .arg(activeCount) + .arg(regularCount) + .arg(visActiveCount) + .arg(visRegularCount) + .arg(foreignCount) + .arg(visForeignCount) + .arg(backupCount) + .arg(visBackupCount) + .arg(separatorCount) + .arg(visSeparatorCount) + ); +} + +void MainWindow::updatePluginCount() +{ + int activeMasterCount = 0; + int activeLightMasterCount = 0; + int activeRegularCount = 0; + int masterCount = 0; + int lightMasterCount = 0; + int regularCount = 0; + int activeVisibleCount = 0; + + PluginList *list = m_OrganizerCore.pluginList(); + QString filter = ui->espFilterEdit->text(); + + for (QString plugin : list->pluginNames()) { + bool active = list->isEnabled(plugin); + bool visible = m_PluginListSortProxy->filterMatchesPlugin(plugin); + if (list->isLight(plugin) || list->isLightFlagged(plugin)) { + lightMasterCount++; + activeLightMasterCount += active; + activeVisibleCount += visible && active; + } else if (list->isMaster(plugin)) { + masterCount++; + activeMasterCount += active; + activeVisibleCount += visible && active; + } else { + regularCount++; + activeRegularCount += active; + activeVisibleCount += visible && active; + } + } + + int activeCount = activeMasterCount + activeLightMasterCount + activeRegularCount; + int totalCount = masterCount + lightMasterCount + regularCount; + + ui->activePluginsCounter->display(activeVisibleCount); + ui->activePluginsCounter->setToolTip(tr("" + "" + "" + "" + "" + "" + "" + "
    TypeActive Total
    All plugins:%1 %2
    ESMs:%3 %4
    ESPs:%7 %8
    ESMs+ESPs:%9 %10
    ESLs:%5 %6
    ") + .arg(activeCount).arg(totalCount) + .arg(activeMasterCount).arg(masterCount) + .arg(activeLightMasterCount).arg(lightMasterCount) + .arg(activeRegularCount).arg(regularCount) + .arg(activeMasterCount+activeRegularCount).arg(masterCount+regularCount) + ); +} + +void MainWindow::information_clicked() +{ + try { + displayModInformation(m_ContextRow); + } catch (const std::exception &e) { + reportError(e.what()); + } +} + +void MainWindow::createEmptyMod_clicked() +{ + GuessedValue name; + name.setFilter(&fixDirectoryName); + + while (name->isEmpty()) { + bool ok; + name.update(QInputDialog::getText(this, tr("Create Mod..."), + tr("This will create an empty mod.\n" + "Please enter a name:"), QLineEdit::Normal, "", &ok), + GUESS_USER); + if (!ok) { + return; + } + } + + if (m_OrganizerCore.getMod(name) != nullptr) { + reportError(tr("A mod with this name already exists")); + return; + } + + int newPriority = -1; + if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) { + newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); + } + + IModInterface *newMod = m_OrganizerCore.createMod(name); + if (newMod == nullptr) { + return; + } + + m_OrganizerCore.refreshModList(); + + if (newPriority >= 0) { + m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); + } +} + +void MainWindow::createSeparator_clicked() +{ + GuessedValue name; + name.setFilter(&fixDirectoryName); + while (name->isEmpty()) + { + bool ok; + name.update(QInputDialog::getText(this, tr("Create Separator..."), + tr("This will create a new separator.\n" + "Please enter a name:"), QLineEdit::Normal, "", &ok), + GUESS_USER); + if (!ok) { return; } + } + if (m_OrganizerCore.getMod(name) != nullptr) + { + reportError(tr("A separator with this name already exists")); + return; + } + name->append("_separator"); + if (m_OrganizerCore.getMod(name) != nullptr) + { + return; + } + + int newPriority = -1; + if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) + { + newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); + } + + if (m_OrganizerCore.createMod(name) == nullptr) { return; } + m_OrganizerCore.refreshModList(); + + if (newPriority >= 0) + { + m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); + } + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QColor previousColor = settings.value("previousSeparatorColor", QColor()).value(); + if (previousColor.isValid()) { + ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(previousColor); + } + +} + +void MainWindow::setColor_clicked() +{ + QSettings &settings = m_OrganizerCore.settings().directInterface(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + QColorDialog dialog(this); + dialog.setOption(QColorDialog::ShowAlphaChannel); + QColor currentColor = modInfo->getColor(); + QColor previousColor = settings.value("previousSeparatorColor", QColor()).value(); + if (currentColor.isValid()) + dialog.setCurrentColor(currentColor); + else + dialog.setCurrentColor(previousColor); + if (!dialog.exec()) + return; + currentColor = dialog.currentColor(); + if (!currentColor.isValid()) + return; + settings.setValue("previousSeparatorColor", currentColor); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + auto flags = info->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + info->setColor(currentColor); + } + } + } + else { + modInfo->setColor(currentColor); + } +} + +void MainWindow::resetColor_clicked() +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + QColor color = QColor(); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + auto flags = info->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + info->setColor(color); + } + } + } + else { + modInfo->setColor(color); + } + Settings::instance().directInterface().remove("previousSeparatorColor"); +} + +void MainWindow::createModFromOverwrite() +{ + GuessedValue name; + name.setFilter(&fixDirectoryName); + + while (name->isEmpty()) { + bool ok; + name.update(QInputDialog::getText(this, tr("Create Mod..."), + tr("This will move all files from overwrite into a new, regular mod.\n" + "Please enter a name:"), QLineEdit::Normal, "", &ok), + GUESS_USER); + if (!ok) { + return; + } + } + + if (m_OrganizerCore.getMod(name) != nullptr) { + reportError(tr("A mod with this name already exists")); + return; + } + + const IModInterface *newMod = m_OrganizerCore.createMod(name); + if (newMod == nullptr) { + return; + } + + doMoveOverwriteContentToMod(newMod->absolutePath()); +} + +void MainWindow::moveOverwriteContentToExistingMod() +{ + QStringList mods; + auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); + for (auto & iter : indexesByPriority) { + if ((iter.second != UINT_MAX)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(iter.second); + if (!modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN) && !modInfo->hasFlag(ModInfo::FLAG_OVERWRITE)) { + mods << modInfo->name(); + } + } + } + + ListDialog dialog(this); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog.objectName()); + + dialog.setWindowTitle("Select a mod..."); + dialog.setChoices(mods); + + if (settings.contains(key)) { + dialog.restoreGeometry(settings.value(key).toByteArray()); + } + if (dialog.exec() == QDialog::Accepted) { + + QString result = dialog.getChoice(); + if (!result.isEmpty()) { + + QString modAbsolutePath; + + for (const auto& mod : m_OrganizerCore.modsSortedByProfilePriority()) { + if (result.compare(mod) == 0) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(mod)); + modAbsolutePath = modInfo->absolutePath(); + break; + } + } + + if (modAbsolutePath.isNull()) { + qWarning("Mod %s has not been found, for some reason", qUtf8Printable(result)); + return; + } + + doMoveOverwriteContentToMod(modAbsolutePath); + } + } + settings.setValue(key, dialog.saveGeometry()); +} + +void MainWindow::doMoveOverwriteContentToMod(const QString &modAbsolutePath) +{ + unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { + std::vector flags = mod->getFlags(); + return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); + + ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex); + bool successful = shellMove((QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"), + (QDir::toNativeSeparators(modAbsolutePath)), false, this); + + if (successful) { + MessageDialog::showMessage(tr("Move successful."), this); + } + else { + qCritical("Move operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError()))); + } + + m_OrganizerCore.refreshModList(); +} + +void MainWindow::clearOverwrite() +{ + unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { + std::vector flags = mod->getFlags(); + return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) + != flags.end(); + }); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); + if (modInfo) + { + QDir overwriteDir(modInfo->absolutePath()); + if (QMessageBox::question(this, tr("Are you sure?"), + tr("About to recursively delete:\n") + overwriteDir.absolutePath(), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) + { + QStringList delList; + for (auto f : overwriteDir.entryList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot)) + delList.push_back(overwriteDir.absoluteFilePath(f)); + shellDelete(delList, true); + updateProblemsButton(); + } + } +} + +void MainWindow::cancelModListEditor() +{ + ui->modList->setEnabled(false); + ui->modList->setEnabled(true); +} + +void MainWindow::on_modList_doubleClicked(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + if (m_OrganizerCore.modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { + // don't interpret double click if we only just checked a mod + return; + } + + QModelIndex sourceIdx = mapToModel(m_OrganizerCore.modList(), index); + if (!sourceIdx.isValid()) { + return; + } + + Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); + if (modifiers.testFlag(Qt::ControlModifier)) { + try { + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + openExplorer_clicked(); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->modList->closePersistentEditor(index); + } + catch (const std::exception &e) { + reportError(e.what()); + } + } + else { + try { + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + sourceIdx.column(); + int tab = -1; + switch (sourceIdx.column()) { + case ModList::COL_NOTES: tab = ModInfoDialog::TAB_NOTES; break; + case ModList::COL_VERSION: tab = ModInfoDialog::TAB_NEXUS; break; + case ModList::COL_MODID: tab = ModInfoDialog::TAB_NEXUS; break; + case ModList::COL_GAME: tab = ModInfoDialog::TAB_NEXUS; break; + case ModList::COL_CATEGORY: tab = ModInfoDialog::TAB_CATEGORIES; break; + case ModList::COL_FLAGS: tab = ModInfoDialog::TAB_CONFLICTS; break; + default: tab = -1; + } + displayModInformation(sourceIdx.row(), tab); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->modList->closePersistentEditor(index); + } + catch (const std::exception &e) { + reportError(e.what()); + } + } +} + +void MainWindow::on_listOptionsBtn_pressed() +{ + m_ContextRow = -1; +} + +void MainWindow::openOriginInformation_clicked() +{ + try { + QItemSelectionModel *selection = ui->espList->selectionModel(); + //we don't want to open multiple modinfodialogs. + /*if (selection->hasSelection() && selection->selectedRows().count() > 0) { + + for (QModelIndex idx : selection->selectedRows()) { + QString fileName = idx.data().toString(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + } + } + } + else {}*/ + QModelIndex idx = selection->currentIndex(); + QString fileName = idx.data().toString(); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + } + } + catch (const std::exception &e) { + reportError(e.what()); + } +} + +void MainWindow::on_espList_doubleClicked(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + if (m_OrganizerCore.pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { + // don't interpret double click if we only just checked a plugin + return; + } + + QModelIndex sourceIdx = mapToModel(m_OrganizerCore.pluginList(), index); + if (!sourceIdx.isValid()) { + return; + } + try { + + QItemSelectionModel *selection = ui->espList->selectionModel(); + + if (selection->hasSelection() && selection->selectedRows().count() == 1) { + + QModelIndex idx = selection->currentIndex(); + QString fileName = idx.data().toString(); + + if (ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)) == UINT_MAX) + return; + + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + + Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); + if (modifiers.testFlag(Qt::ControlModifier)) { + openExplorer_activated(); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->espList->closePersistentEditor(index); + } + else { + + displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->espList->closePersistentEditor(index); + } + } + } + } + catch (const std::exception &e) { + reportError(e.what()); + } +} + +bool MainWindow::populateMenuCategories(QMenu *menu, int targetID) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + const std::set &categories = modInfo->getCategories(); + + bool childEnabled = false; + + for (unsigned int i = 1; i < m_CategoryFactory.numCategories(); ++i) { + if (m_CategoryFactory.getParentID(i) == targetID) { + QMenu *targetMenu = menu; + if (m_CategoryFactory.hasChildren(i)) { + targetMenu = menu->addMenu(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); + } + + int id = m_CategoryFactory.getCategoryID(i); + QScopedPointer checkBox(new QCheckBox(targetMenu)); + bool enabled = categories.find(id) != categories.end(); + checkBox->setText(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); + if (enabled) { + childEnabled = true; + } + checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); + + QScopedPointer checkableAction(new QWidgetAction(targetMenu)); + checkableAction->setDefaultWidget(checkBox.take()); + checkableAction->setData(id); + targetMenu->addAction(checkableAction.take()); + + if (m_CategoryFactory.hasChildren(i)) { + if (populateMenuCategories(targetMenu, m_CategoryFactory.getCategoryID(i)) || enabled) { + targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); + } + } + } + } + return childEnabled; +} + +void MainWindow::replaceCategoriesFromMenu(QMenu *menu, int modRow) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow); + for (QAction* action : menu->actions()) { + if (action->menu() != nullptr) { + replaceCategoriesFromMenu(action->menu(), modRow); + } else { + QWidgetAction *widgetAction = qobject_cast(action); + if (widgetAction != nullptr) { + QCheckBox *checkbox = qobject_cast(widgetAction->defaultWidget()); + modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked()); + } + } + } +} + +void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow) +{ + if (referenceRow != -1 && referenceRow != modRow) { + ModInfo::Ptr editedModInfo = ModInfo::getByIndex(referenceRow); + for (QAction* action : menu->actions()) { + if (action->menu() != nullptr) { + addRemoveCategoriesFromMenu(action->menu(), modRow, referenceRow); + } else { + QWidgetAction *widgetAction = qobject_cast(action); + if (widgetAction != nullptr) { + QCheckBox *checkbox = qobject_cast(widgetAction->defaultWidget()); + int categoryId = widgetAction->data().toInt(); + bool checkedBefore = editedModInfo->categorySet(categoryId); + bool checkedAfter = checkbox->isChecked(); + + if (checkedBefore != checkedAfter) { // only update if the category was changed on the edited mod + ModInfo::Ptr currentModInfo = ModInfo::getByIndex(modRow); + currentModInfo->setCategory(categoryId, checkedAfter); + } + } + } + } + } else { + replaceCategoriesFromMenu(menu, modRow); + } +} + +void MainWindow::addRemoveCategories_MenuHandler() { + QMenu *menu = qobject_cast(sender()); + if (menu == nullptr) { + qCritical("not a menu?"); + return; + } + + QList selected; + for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { + selected.append(QPersistentModelIndex(idx)); + } + + if (selected.size() > 0) { + int minRow = INT_MAX; + int maxRow = -1; + + for (const QPersistentModelIndex &idx : selected) { + qDebug("change categories on: %s", qUtf8Printable(idx.data().toString())); + QModelIndex modIdx = mapToModel(m_OrganizerCore.modList(), idx); + if (modIdx.row() != m_ContextIdx.row()) { + addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row()); + } + if (idx.row() < minRow) minRow = idx.row(); + if (idx.row() > maxRow) maxRow = idx.row(); + } + replaceCategoriesFromMenu(menu, m_ContextIdx.row()); + + m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); + + for (const QPersistentModelIndex &idx : selected) { + ui->modList->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + } else { + //For single mod selections, just do a replace + replaceCategoriesFromMenu(menu, m_ContextRow); + m_OrganizerCore.modList()->notifyChange(m_ContextRow); + } + + refreshFilters(); +} + +void MainWindow::replaceCategories_MenuHandler() { + QMenu *menu = qobject_cast(sender()); + if (menu == nullptr) { + qCritical("not a menu?"); + return; + } + + QList selected; + for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { + selected.append(QPersistentModelIndex(idx)); + } + + if (selected.size() > 0) { + QStringList selectedMods; + int minRow = INT_MAX; + int maxRow = -1; + for (int i = 0; i < selected.size(); ++i) { + QModelIndex temp = mapToModel(m_OrganizerCore.modList(), selected.at(i)); + selectedMods.append(temp.data().toString()); + replaceCategoriesFromMenu(menu, mapToModel(m_OrganizerCore.modList(), selected.at(i)).row()); + if (temp.row() < minRow) minRow = temp.row(); + if (temp.row() > maxRow) maxRow = temp.row(); + } + + m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); + + // find mods by their name because indices are invalidated + QAbstractItemModel *model = ui->modList->model(); + for (const QString &mod : selectedMods) { + QModelIndexList matches = model->match(model->index(0, 0), Qt::DisplayRole, mod, 1, + Qt::MatchFixedString | Qt::MatchCaseSensitive | Qt::MatchRecursive); + if (matches.size() > 0) { + ui->modList->selectionModel()->select(matches.at(0), QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + } + } else { + //For single mod selections, just do a replace + replaceCategoriesFromMenu(menu, m_ContextRow); + m_OrganizerCore.modList()->notifyChange(m_ContextRow); + } + + refreshFilters(); +} + +void MainWindow::saveArchiveList() +{ + if (m_OrganizerCore.isArchivesInit()) { + SafeWriteFile archiveFile(m_OrganizerCore.currentProfile()->getArchivesFileName()); + for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { + QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i); + 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")); + } + } + } + if (archiveFile.commitIfDifferent(m_ArchiveListHash)) { + qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(m_OrganizerCore.currentProfile()->getArchivesFileName()))); + } + } else { + qWarning("archive list not initialised"); + } +} + +void MainWindow::checkModsForUpdates() +{ + statusBar()->show(); + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); + m_RefreshProgress->setRange(0, m_ModsToUpdate); + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { // otherwise there will be no endorsement info + MessageDialog::showMessage(tr("Not logged in, endorsement information will be wrong"), + this, true); + m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); + } + } +} + +void MainWindow::changeVersioningScheme() { + if (QMessageBox::question(this, tr("Continue?"), + tr("The versioning scheme decides which version is considered newer than another.\n" + "This function will guess the versioning scheme under the assumption that the installed version is outdated."), + QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) { + + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + + bool success = false; + + static VersionInfo::VersionScheme schemes[] = { VersionInfo::SCHEME_REGULAR, VersionInfo::SCHEME_DECIMALMARK, VersionInfo::SCHEME_NUMBERSANDLETTERS }; + + for (int i = 0; i < sizeof(schemes) / sizeof(VersionInfo::VersionScheme) && !success; ++i) { + VersionInfo verOld(info->getVersion().canonicalString(), schemes[i]); + VersionInfo verNew(info->getNewestVersion().canonicalString(), schemes[i]); + if (verOld < verNew) { + info->setVersion(verOld); + info->setNewestVersion(verNew); + success = true; + } + } + if (!success) { + QMessageBox::information(this, tr("Sorry"), + tr("I don't know a versioning scheme where %1 is newer than %2.").arg(info->getNewestVersion().canonicalString()).arg(info->getVersion().canonicalString()), + QMessageBox::Ok); + } + } +} + +void MainWindow::ignoreUpdate() { + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->ignoreUpdate(true); + } + } + else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->ignoreUpdate(true); + } + if (m_ModListSortProxy != nullptr) { + m_ModListSortProxy->invalidate(); + } +} + +void MainWindow::unignoreUpdate() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->ignoreUpdate(false); + } + } + else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->ignoreUpdate(false); + } + if (m_ModListSortProxy != nullptr) { + m_ModListSortProxy->invalidate(); + } +} + +void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, + ModInfo::Ptr info) { + const std::set &categories = info->getCategories(); + for (int categoryID : categories) { + int catIdx = m_CategoryFactory.getCategoryIndex(categoryID); + QWidgetAction *action = new QWidgetAction(primaryCategoryMenu); + try { + QRadioButton *categoryBox = new QRadioButton( + m_CategoryFactory.getCategoryName(catIdx).replace('&', "&&"), + primaryCategoryMenu); + connect(categoryBox, &QRadioButton::toggled, [info, categoryID](bool enable) { + if (enable) { + info->setPrimaryCategory(categoryID); + } + }); + categoryBox->setChecked(categoryID == info->getPrimaryCategory()); + action->setDefaultWidget(categoryBox); + } catch (const std::exception &e) { + qCritical("failed to create category checkbox: %s", e.what()); + } + + action->setData(categoryID); + primaryCategoryMenu->addAction(action); + } +} + +void MainWindow::addPrimaryCategoryCandidates() +{ + QMenu *menu = qobject_cast(sender()); + if (menu == nullptr) { + qCritical("not a menu?"); + return; + } + menu->clear(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + + addPrimaryCategoryCandidates(menu, modInfo); +} + +void MainWindow::enableVisibleMods() +{ + if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_ModListSortProxy->enableAllVisible(); + } +} + +void MainWindow::disableVisibleMods() +{ + if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_ModListSortProxy->disableAllVisible(); + } +} + +void MainWindow::openInstanceFolder() +{ + QString dataPath = qApp->property("dataPath").toString(); + ::ShellExecuteW(nullptr, L"explore", ToWString(dataPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + + //opens BaseDirectory instead + //::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getBaseDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void MainWindow::openLogsFolder() +{ + QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()); + ::ShellExecuteW(nullptr, L"explore", ToWString(logsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void MainWindow::openInstallFolder() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(qApp->applicationDirPath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void MainWindow::openPluginsFolder() +{ + QString pluginsPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()); + ::ShellExecuteW(nullptr, L"explore", ToWString(pluginsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + + +void MainWindow::openProfileFolder() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void MainWindow::openIniFolder() +{ + if (m_OrganizerCore.currentProfile()->localSettingsEnabled()) + { + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + else { + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } +} + +void MainWindow::openDownloadsFolder() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getDownloadDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void MainWindow::openModsFolder() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getModDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void MainWindow::openGameFolder() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void MainWindow::openMyGamesFolder() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + + +void MainWindow::exportModListCSV() +{ + //SelectionDialog selection(tr("Choose what to export")); + + //selection.addChoice(tr("Everything"), tr("All installed mods are included in the list"), 0); + //selection.addChoice(tr("Active Mods"), tr("Only active (checked) mods from your current profile are included"), 1); + //selection.addChoice(tr("Visible"), tr("All mods visible in the mod list are included"), 2); + + QDialog selection(this); + QGridLayout *grid = new QGridLayout; + selection.setWindowTitle(tr("Export to csv")); + + QLabel *csvDescription = new QLabel(); + csvDescription->setText(tr("CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.\nYou can also use online editors and converters instead.")); + grid->addWidget(csvDescription); + + QGroupBox *groupBoxRows = new QGroupBox(tr("Select what mods you want export:")); + QRadioButton *all = new QRadioButton(tr("All installed mods")); + QRadioButton *active = new QRadioButton(tr("Only active (checked) mods from your current profile")); + QRadioButton *visible = new QRadioButton(tr("All currently visible mods in the mod list")); + + QVBoxLayout *vbox = new QVBoxLayout; + vbox->addWidget(all); + vbox->addWidget(active); + vbox->addWidget(visible); + vbox->addStretch(1); + groupBoxRows->setLayout(vbox); + + + + grid->addWidget(groupBoxRows); + + QButtonGroup *buttonGroupRows = new QButtonGroup(); + buttonGroupRows->addButton(all, 0); + buttonGroupRows->addButton(active, 1); + buttonGroupRows->addButton(visible, 2); + buttonGroupRows->button(0)->setChecked(true); + + + + QGroupBox *groupBoxColumns = new QGroupBox(tr("Choose what Columns to export:")); + groupBoxColumns->setFlat(true); + + QCheckBox *mod_Priority = new QCheckBox(tr("Mod_Priority")); + mod_Priority->setChecked(true); + QCheckBox *mod_Name = new QCheckBox(tr("Mod_Name")); + mod_Name->setChecked(true); + QCheckBox *mod_Note = new QCheckBox(tr("Notes_column")); + QCheckBox *mod_Status = new QCheckBox(tr("Mod_Status")); + QCheckBox *primary_Category = new QCheckBox(tr("Primary_Category")); + QCheckBox *nexus_ID = new QCheckBox(tr("Nexus_ID")); + QCheckBox *mod_Nexus_URL = new QCheckBox(tr("Mod_Nexus_URL")); + QCheckBox *mod_Version = new QCheckBox(tr("Mod_Version")); + QCheckBox *install_Date = new QCheckBox(tr("Install_Date")); + QCheckBox *download_File_Name = new QCheckBox(tr("Download_File_Name")); + + QVBoxLayout *vbox1 = new QVBoxLayout; + vbox1->addWidget(mod_Priority); + vbox1->addWidget(mod_Name); + vbox1->addWidget(mod_Status); + vbox1->addWidget(mod_Note); + vbox1->addWidget(primary_Category); + vbox1->addWidget(nexus_ID); + vbox1->addWidget(mod_Nexus_URL); + vbox1->addWidget(mod_Version); + vbox1->addWidget(install_Date); + vbox1->addWidget(download_File_Name); + groupBoxColumns->setLayout(vbox1); + + grid->addWidget(groupBoxColumns); + + QPushButton *ok = new QPushButton("Ok"); + QPushButton *cancel = new QPushButton("Cancel"); + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + connect(buttons, SIGNAL(accepted()), &selection, SLOT(accept())); + connect(buttons, SIGNAL(rejected()), &selection, SLOT(reject())); + + grid->addWidget(buttons); + + selection.setLayout(grid); + + + if (selection.exec() == QDialog::Accepted) { + + unsigned int numMods = ModInfo::getNumMods(); + int selectedRowID = buttonGroupRows->checkedId(); + + try { + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + CSVBuilder builder(&buffer); + builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS); + std::vector > fields; + if (mod_Priority->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Priority"), CSVBuilder::TYPE_STRING)); + if (mod_Name->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Name"), CSVBuilder::TYPE_STRING)); + if (mod_Status->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Status"), CSVBuilder::TYPE_STRING)); + if (mod_Note->isChecked()) + fields.push_back(std::make_pair(QString("#Note"), CSVBuilder::TYPE_STRING)); + if (primary_Category->isChecked()) + fields.push_back(std::make_pair(QString("#Primary_Category"), CSVBuilder::TYPE_STRING)); + if (nexus_ID->isChecked()) + fields.push_back(std::make_pair(QString("#Nexus_ID"), CSVBuilder::TYPE_INTEGER)); + if (mod_Nexus_URL->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Nexus_URL"), CSVBuilder::TYPE_STRING)); + if (mod_Version->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Version"), CSVBuilder::TYPE_STRING)); + if (install_Date->isChecked()) + fields.push_back(std::make_pair(QString("#Install_Date"), CSVBuilder::TYPE_STRING)); + if (download_File_Name->isChecked()) + fields.push_back(std::make_pair(QString("#Download_File_Name"), CSVBuilder::TYPE_STRING)); + + builder.setFields(fields); + + builder.writeHeader(); + + for (unsigned int i = 0; i < numMods; ++i) { + ModInfo::Ptr info = ModInfo::getByIndex(i); + bool enabled = m_OrganizerCore.currentProfile()->modEnabled(i); + if ((selectedRowID == 1) && !enabled) { + continue; + } + else if ((selectedRowID == 2) && !m_ModListSortProxy->filterMatchesMod(info, enabled)) { + continue; + } + std::vector flags = info->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end()) && + (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end())) { + if (mod_Priority->isChecked()) + builder.setRowField("#Mod_Priority", QString("%1").arg(m_OrganizerCore.currentProfile()->getModPriority(i), 4, 10, QChar('0'))); + if (mod_Name->isChecked()) + builder.setRowField("#Mod_Name", info->name()); + if (mod_Status->isChecked()) + builder.setRowField("#Mod_Status", (enabled)? "Enabled" : "Disabled"); + if (mod_Note->isChecked()) + builder.setRowField("#Note", QString("%1").arg(info->comments().remove(','))); + if (primary_Category->isChecked()) + builder.setRowField("#Primary_Category", (m_CategoryFactory.categoryExists(info->getPrimaryCategory())) ? m_CategoryFactory.getCategoryName(info->getPrimaryCategory()) : ""); + if (nexus_ID->isChecked()) + builder.setRowField("#Nexus_ID", info->getNexusID()); + if (mod_Nexus_URL->isChecked()) + builder.setRowField("#Mod_Nexus_URL",(info->getNexusID()>0)? NexusInterface::instance(&m_PluginContainer)->getModURL(info->getNexusID(), info->getGameName()) : ""); + if (mod_Version->isChecked()) + builder.setRowField("#Mod_Version", info->getVersion().canonicalString()); + if (install_Date->isChecked()) + builder.setRowField("#Install_Date", info->creationTime().toString("yyyy/MM/dd HH:mm:ss")); + if (download_File_Name->isChecked()) + builder.setRowField("#Download_File_Name", info->getInstallationFile()); + + builder.writeRow(); + } + } + + SaveTextAsDialog saveDialog(this); + saveDialog.setText(buffer.data()); + saveDialog.exec(); + } + catch (const std::exception &e) { + reportError(tr("export failed: %1").arg(e.what())); + } + } +} + +static void addMenuAsPushButton(QMenu *menu, QMenu *subMenu) +{ + QPushButton *pushBtn = new QPushButton(subMenu->title()); + pushBtn->setMenu(subMenu); + QWidgetAction *action = new QWidgetAction(menu); + action->setDefaultWidget(pushBtn); + menu->addAction(action); +} + +QMenu *MainWindow::openFolderMenu() +{ + + QMenu *FolderMenu = new QMenu(this); + + FolderMenu->addAction(tr("Open Game folder"), this, SLOT(openGameFolder())); + + FolderMenu->addAction(tr("Open MyGames folder"), this, SLOT(openMyGamesFolder())); + + FolderMenu->addAction(tr("Open INIs folder"), this, SLOT(openIniFolder())); + + FolderMenu->addSeparator(); + + FolderMenu->addAction(tr("Open Instance folder"), this, SLOT(openInstanceFolder())); + + FolderMenu->addAction(tr("Open Mods folder"), this, SLOT(openModsFolder())); + + FolderMenu->addAction(tr("Open Profile folder"), this, SLOT(openProfileFolder())); + + FolderMenu->addAction(tr("Open Downloads folder"), this, SLOT(openDownloadsFolder())); + + FolderMenu->addSeparator(); + + FolderMenu->addAction(tr("Open MO2 Install folder"), this, SLOT(openInstallFolder())); + + FolderMenu->addAction(tr("Open MO2 Plugins folder"), this, SLOT(openPluginsFolder())); + + FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder())); + + + return FolderMenu; +} + +void MainWindow::initModListContextMenu(QMenu *menu) +{ + menu->addAction(tr("Install Mod..."), this, SLOT(installMod_clicked())); + menu->addAction(tr("Create empty mod"), this, SLOT(createEmptyMod_clicked())); + menu->addAction(tr("Create Separator"), this, SLOT(createSeparator_clicked())); + + menu->addSeparator(); + + menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); + menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); + menu->addAction(tr("Check all for update"), this, SLOT(checkModsForUpdates())); + menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh())); + menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); +} + +void MainWindow::addModSendToContextMenu(QMenu *menu) +{ + if (m_ModListSortProxy->sortColumn() != ModList::COL_PRIORITY) + return; + + QMenu *sub_menu = new QMenu(menu); + sub_menu->setTitle(tr("Send to")); + sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedModsToTop_clicked())); + sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedModsToBottom_clicked())); + sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedModsToPriority_clicked())); + sub_menu->addAction(tr("Separator..."), this, SLOT(sendSelectedModsToSeparator_clicked())); + + menu->addMenu(sub_menu); + menu->addSeparator(); +} + +void MainWindow::addPluginSendToContextMenu(QMenu *menu) +{ + if (m_PluginListSortProxy->sortColumn() != PluginList::COL_PRIORITY) + return; + + QMenu *sub_menu = new QMenu(this); + sub_menu->setTitle(tr("Send to")); + sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedPluginsToTop_clicked())); + sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedPluginsToBottom_clicked())); + sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedPluginsToPriority_clicked())); + + menu->addMenu(sub_menu); + menu->addSeparator(); +} + +void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) +{ + try { + QTreeView *modList = findChild("modList"); + + m_ContextIdx = mapToModel(m_OrganizerCore.modList(), modList->indexAt(pos)); + m_ContextRow = m_ContextIdx.row(); + + if (m_ContextRow == -1) { + // no selection + QMenu menu(this); + initModListContextMenu(&menu); + menu.exec(modList->mapToGlobal(pos)); + } else { + QMenu menu(this); + + QMenu *allMods = new QMenu(&menu); + initModListContextMenu(allMods); + allMods->setTitle(tr("All Mods")); + menu.addMenu(allMods); + menu.addSeparator(); + + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + std::vector flags = info->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { + if (QDir(info->absolutePath()).count() > 2) { + menu.addAction(tr("Sync to Mods..."), &m_OrganizerCore, SLOT(syncOverwrite())); + menu.addAction(tr("Create Mod..."), this, SLOT(createModFromOverwrite())); + menu.addAction(tr("Move content to Mod..."), this, SLOT(moveOverwriteContentToExistingMod())); + menu.addAction(tr("Clear Overwrite..."), this, SLOT(clearOverwrite())); + } + menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); + } 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_SEPARATOR) != flags.end()){ + menu.addSeparator(); + QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu); + populateMenuCategories(addRemoveCategoriesMenu, 0); + connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler())); + addMenuAsPushButton(&menu, addRemoveCategoriesMenu); + QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu); + connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); + addMenuAsPushButton(&menu, primaryCategoryMenu); + menu.addSeparator(); + menu.addAction(tr("Rename Separator..."), this, SLOT(renameMod_clicked())); + menu.addAction(tr("Remove Separator..."), this, SLOT(removeMod_clicked())); + menu.addSeparator(); + addModSendToContextMenu(&menu); + menu.addAction(tr("Select Color..."), this, SLOT(setColor_clicked())); + if(info->getColor().isValid()) + menu.addAction(tr("Reset Color"), this, SLOT(resetColor_clicked())); + menu.addSeparator(); + } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) { + addModSendToContextMenu(&menu); + } else { + QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu); + populateMenuCategories(addRemoveCategoriesMenu, 0); + connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler())); + addMenuAsPushButton(&menu, addRemoveCategoriesMenu); + + QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu); + connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); + addMenuAsPushButton(&menu, primaryCategoryMenu); + + menu.addSeparator(); + + if (info->downgradeAvailable()) { + menu.addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme())); + } + + if (info->updateIgnored()) { + menu.addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate())); + } + else { + if (info->updateAvailable() || info->downgradeAvailable()) { + menu.addAction(tr("Ignore update"), this, SLOT(ignoreUpdate())); + } + } + menu.addSeparator(); + + menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedMods_clicked())); + menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedMods_clicked())); + + menu.addSeparator(); + + addModSendToContextMenu(&menu); + + menu.addAction(tr("Rename Mod..."), this, SLOT(renameMod_clicked())); + menu.addAction(tr("Reinstall Mod"), this, SLOT(reinstallMod_clicked())); + menu.addAction(tr("Remove Mod..."), this, SLOT(removeMod_clicked())); + menu.addAction(tr("Create Backup"), this, SLOT(backupMod_clicked())); + + menu.addSeparator(); + + if (info->getNexusID() > 0 && Settings::instance().endorsementIntegration()) { + switch (info->endorsedState()) { + case ModInfo::ENDORSED_TRUE: { + menu.addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked())); + } break; + case ModInfo::ENDORSED_FALSE: { + menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); + menu.addAction(tr("Won't endorse"), this, SLOT(dontendorse_clicked())); + } break; + case ModInfo::ENDORSED_NEVER: { + menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); + } break; + default: { + QAction *action = new QAction(tr("Endorsement state unknown"), &menu); + action->setEnabled(false); + menu.addAction(action); + } break; + } + } + + menu.addSeparator(); + + std::vector flags = info->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) { + menu.addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked())); + } + + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) { + menu.addAction(tr("Mark as converted/working"), this, SLOT(markConverted_clicked())); + } + + if (info->getNexusID() > 0) { + menu.addAction(tr("Visit on Nexus"), this, SLOT(visitOnNexus_clicked())); + } else if ((info->getURL() != "")) { + menu.addAction(tr("Visit web page"), this, SLOT(visitWebPage_clicked())); + } + + menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); + } + + 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)); + } + } catch (const std::exception &e) { + reportError(tr("Exception: ").arg(e.what())); + } catch (...) { + reportError(tr("Unknown exception")); + } +} + + +void MainWindow::on_categoriesList_itemSelectionChanged() +{ + QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows(); + std::vector categories; + std::vector content; + for (const QModelIndex &index : indices) { + int filterType = index.data(Qt::UserRole + 1).toInt(); + if ((filterType == ModListSortProxy::TYPE_CATEGORY) + || (filterType == ModListSortProxy::TYPE_SPECIAL)) { + int categoryId = index.data(Qt::UserRole).toInt(); + if (categoryId != CategoryFactory::CATEGORY_NONE) { + categories.push_back(categoryId); + } + } else if (filterType == ModListSortProxy::TYPE_CONTENT) { + int contentId = index.data(Qt::UserRole).toInt(); + content.push_back(contentId); + } + } + + m_ModListSortProxy->setCategoryFilter(categories); + m_ModListSortProxy->setContentFilter(content); + ui->clickBlankButton->setEnabled(categories.size() > 0 || content.size() >0); + + if (indices.count() == 0) { + ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); + } else if (indices.count() > 1) { + ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); + } else { + ui->currentCategoryLabel->setText(QString("(%1)").arg(indices.first().data().toString())); + } + ui->modList->reset(); +} + + +void MainWindow::deleteSavegame_clicked() +{ + SaveGameInfo const *info = m_OrganizerCore.managedGame()->feature(); + + QString savesMsgLabel; + QStringList deleteFiles; + + int count = 0; + + for (const QModelIndex &idx : ui->savegameList->selectionModel()->selectedIndexes()) { + QString name = idx.data(Qt::UserRole).toString(); + + if (count < 10) { + savesMsgLabel += "
  • " + QFileInfo(name).completeBaseName() + "
  • "; + } + ++count; + + if (info == nullptr) { + deleteFiles.push_back(name); + } else { + ISaveGame const *save = info->getSaveGameInfo(name); + deleteFiles += save->allFiles(); + delete save; + } + } + + if (count > 10) { + savesMsgLabel += "
  • ... " + tr("%1 more").arg(count - 10) + "
  • "; + } + + if (QMessageBox::question(this, tr("Confirm"), + tr("Are you sure you want to remove the following %n save(s)?
    " + "
      %1

    " + "Removed saves will be sent to the Recycle Bin.", "", count) + .arg(savesMsgLabel), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + shellDelete(deleteFiles, true); // recycle bin delete. + refreshSaveList(); + } +} + + +void MainWindow::fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets) +{ + ActivateModsDialog dialog(missingAssets, this); + if (dialog.exec() == QDialog::Accepted) { + // activate the required mods, then enable all esps + std::set modsToActivate = dialog.getModsToActivate(); + for (std::set::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) { + if ((*iter != "") && (*iter != "")) { + unsigned int modIndex = ModInfo::getIndex(*iter); + m_OrganizerCore.currentProfile()->setModEnabled(modIndex, true); + } + } + + m_OrganizerCore.currentProfile()->writeModlist(); + m_OrganizerCore.refreshLists(); + + std::set espsToActivate = dialog.getESPsToActivate(); + for (std::set::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) { + m_OrganizerCore.pluginList()->enableESP(*iter); + } + m_OrganizerCore.saveCurrentLists(); + } +} + + +void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos) +{ + QItemSelectionModel *selection = ui->savegameList->selectionModel(); + + if (!selection->hasSelection()) { + return; + } + + QMenu menu; + QAction *action = menu.addAction(tr("Enable Mods...")); + action->setEnabled(false); + + if (selection->selectedIndexes().count() == 1) { + SaveGameInfo const *info = this->m_OrganizerCore.managedGame()->feature(); + if (info != nullptr) { + QString save = ui->savegameList->currentItem()->data(Qt::UserRole).toString(); + SaveGameInfo::MissingAssets missing = info->getMissingAssets(save); + if (missing.size() != 0) { + connect(action, &QAction::triggered, this, [this, missing]{ fixMods_clicked(missing); }); + action->setEnabled(true); + } + } + } + + QString deleteMenuLabel = tr("Delete %n save(s)", "", selection->selectedIndexes().count()); + + menu.addAction(deleteMenuLabel, this, SLOT(deleteSavegame_clicked())); + + menu.exec(ui->savegameList->mapToGlobal(pos)); +} + +void MainWindow::linkToolbar() +{ + Executable &exe(getSelectedExecutable()); + exe.showOnToolbar(!exe.isShownOnToolbar()); + ui->linkButton->menu()->actions().at(static_cast(ShortcutType::Toolbar))->setIcon(exe.isShownOnToolbar() ? QIcon(":/MO/gui/remove") : QIcon(":/MO/gui/link")); + updateToolBar(); +} + +namespace { +QString getLinkfile(const QString &dir, const Executable &exec) +{ + return QDir::fromNativeSeparators(dir) + "/" + exec.m_Title + ".lnk"; +} + +QString getDesktopLinkfile(const Executable &exec) +{ + return getLinkfile(getDesktopDirectory(), exec); +} + +QString getStartMenuLinkfile(const Executable &exec) +{ + return getLinkfile(getStartMenuDirectory(), exec); +} +} + +void MainWindow::addWindowsLink(const ShortcutType mapping) +{ + const Executable &selectedExecutable(getSelectedExecutable()); + QString const linkName = getLinkfile(mapping == ShortcutType::Desktop ? getDesktopDirectory() : getStartMenuDirectory(), + selectedExecutable); + + if (QFile::exists(linkName)) { + if (QFile::remove(linkName)) { + ui->linkButton->menu()->actions().at(static_cast(mapping))->setIcon(QIcon(":/MO/gui/link")); + } else { + reportError(tr("failed to remove %1").arg(linkName)); + } + } else { + QFileInfo const exeInfo(qApp->applicationFilePath()); + // create link + QString executable = QDir::toNativeSeparators(selectedExecutable.m_BinaryInfo.absoluteFilePath()); + + std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); + std::wstring parameter = ToWString( + QString("\"moshortcut://%1:%2\"").arg(InstanceManager::instance().currentInstance(),selectedExecutable.m_Title)); + std::wstring description = ToWString(QString("Run %1 with ModOrganizer").arg(selectedExecutable.m_Title)); + std::wstring iconFile = ToWString(executable); + std::wstring currentDirectory = ToWString(QDir::toNativeSeparators(qApp->applicationDirPath())); + + if (CreateShortcut(targetFile.c_str() + , parameter.c_str() + , QDir::toNativeSeparators(linkName).toUtf8().constData() + , description.c_str() + , (selectedExecutable.usesOwnIcon() ? iconFile.c_str() : nullptr), 0 + , currentDirectory.c_str()) == 0) { + ui->linkButton->menu()->actions().at(static_cast(mapping))->setIcon(QIcon(":/MO/gui/remove")); + } else { + reportError(tr("failed to create %1").arg(linkName)); + } + } +} + +void MainWindow::linkDesktop() +{ + addWindowsLink(ShortcutType::Desktop); +} + +void MainWindow::linkMenu() +{ + addWindowsLink(ShortcutType::StartMenu); +} + +void MainWindow::on_actionSettings_triggered() +{ + Settings &settings = m_OrganizerCore.settings(); + + QString oldModDirectory(settings.getModDirectory()); + QString oldCacheDirectory(settings.getCacheDirectory()); + QString oldProfilesDirectory(settings.getProfileDirectory()); + QString oldManagedGameDirectory(settings.getManagedGameDirectory()); + bool oldDisplayForeign(settings.displayForeign()); + bool proxy = settings.useProxy(); + DownloadManager *dlManager = m_OrganizerCore.downloadManager(); + + settings.query(&m_PluginContainer, this); + + if (oldManagedGameDirectory != settings.getManagedGameDirectory()) { + QMessageBox::about(this, tr("Restarting MO"), + tr("Changing the managed game directory requires restarting MO.\n" + "Any pending downloads will be paused.\n\n" + "Click OK to restart MO now.")); + dlManager->pauseAll(); + qApp->exit(INT_MAX); + } + + InstallationManager *instManager = m_OrganizerCore.installationManager(); + instManager->setModsDirectory(settings.getModDirectory()); + instManager->setDownloadDirectory(settings.getDownloadDirectory()); + + fixCategories(); + refreshFilters(); + + if (settings.getProfileDirectory() != oldProfilesDirectory) { + refreshProfiles(); + } + + if (dlManager->getOutputDirectory() != settings.getDownloadDirectory()) { + if (dlManager->downloadsInProgress()) { + MessageDialog::showMessage(tr("Can't change download directory while " + "downloads are in progress!"), + this); + } else { + dlManager->setOutputDirectory(settings.getDownloadDirectory()); + } + } + dlManager->setPreferredServers(settings.getPreferredServers()); + + if ((settings.getModDirectory() != oldModDirectory) + || (settings.displayForeign() != oldDisplayForeign)) { + m_OrganizerCore.profileRefresh(); + } + + const auto state = settings.archiveParsing(); + if (state != m_OrganizerCore.getArchiveParsing()) + { + m_OrganizerCore.setArchiveParsing(state); + if (!state) + { + ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked); + ui->showArchiveDataCheckBox->setEnabled(false); + m_showArchiveData = false; + } + else + { + ui->showArchiveDataCheckBox->setCheckState(Qt::Checked); + ui->showArchiveDataCheckBox->setEnabled(true); + m_showArchiveData = true; + } + m_OrganizerCore.refreshModList(); + m_OrganizerCore.refreshDirectoryStructure(); + m_OrganizerCore.refreshLists(); + } + + if (settings.getCacheDirectory() != oldCacheDirectory) { + NexusInterface::instance(&m_PluginContainer)->setCacheDirectory(settings.getCacheDirectory()); + } + + if (proxy != settings.useProxy()) { + activateProxy(settings.useProxy()); + } + + NexusInterface::instance(&m_PluginContainer)->setNMMVersion(settings.getNMMVersion()); + + updateDownloadView(); + + m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist()); + m_OrganizerCore.cycleDiagnostics(); +} + + +void MainWindow::on_actionNexus_triggered() +{ + const IPluginGame *game = m_OrganizerCore.managedGame(); + QString gameName = game->gameShortName(); + if (game->gameNexusName().isEmpty() && game->primarySources().count()) + gameName = game->primarySources()[0]; + QDesktopServices::openUrl(QUrl(NexusInterface::instance(&m_PluginContainer)->getGameURL(gameName))); +} + + +void MainWindow::linkClicked(const QString &url) +{ + QDesktopServices::openUrl(QUrl(url)); +} + + +void MainWindow::installTranslator(const QString &name) +{ + QTranslator *translator = new QTranslator(this); + QString fileName = name + "_" + m_CurrentLanguage; + if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) { + if (m_CurrentLanguage.compare("en", Qt::CaseInsensitive)) { + qDebug("localization file %s not found", qUtf8Printable(fileName)); + } // we don't actually expect localization files for English + } + + qApp->installTranslator(translator); + m_Translators.push_back(translator); +} + + +void MainWindow::languageChange(const QString &newLanguage) +{ + for (QTranslator *trans : m_Translators) { + qApp->removeTranslator(trans); + } + m_Translators.clear(); + + m_CurrentLanguage = newLanguage; + + installTranslator("qt"); + installTranslator("qtbase"); + installTranslator(ToQString(AppConfig::translationPrefix())); + for (const QString &fileName : m_PluginContainer.pluginFileNames()) { + installTranslator(QFileInfo(fileName).baseName()); + } + ui->retranslateUi(this); + qDebug("loaded language %s", qUtf8Printable(newLanguage)); + + ui->profileBox->setItemText(0, QObject::tr("")); + + createHelpWidget(); + + updateDownloadView(); + updateProblemsButton(); + + QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn); + initModListContextMenu(listOptionsMenu); + ui->listOptionsBtn->setMenu(listOptionsMenu); + + ui->openFolderMenu->setMenu(openFolderMenu()); +} + +void MainWindow::writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry) +{ + for (FileEntry::Ptr current : directoryEntry.getFiles()) { + bool isArchive = false; + int origin = current->getOrigin(isArchive); + if (isArchive) { + // TODO: don't list files from archives. maybe make this an option? + continue; + } + QString fullName = directory + "\\" + ToQString(current->getName()); + file.write(fullName.toUtf8()); + + file.write("\t("); + file.write(ToQString(m_OrganizerCore.directoryStructure()->getOriginByID(origin).getName()).toUtf8()); + file.write(")\r\n"); + } + + // recurse into subdirectories + std::vector::const_iterator current, end; + directoryEntry.getSubDirectories(current, end); + for (; current != end; ++current) { + writeDataToFile(file, directory + "\\" + ToQString((*current)->getName()), **current); + } +} + +void MainWindow::writeDataToFile() +{ + QString fileName = QFileDialog::getSaveFileName(this); + if (!fileName.isEmpty()) { + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + reportError(tr("failed to write to file %1").arg(fileName)); + } + + writeDataToFile(file, "data", *m_OrganizerCore.directoryStructure()); + file.close(); + + MessageDialog::showMessage(tr("%1 written").arg(QDir::toNativeSeparators(fileName)), this); + } +} + + +int MainWindow::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) +{ + QString extension = targetInfo.suffix(); + if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || + (extension.compare("com", Qt::CaseInsensitive) == 0) || + (extension.compare("bat", Qt::CaseInsensitive) == 0)) { + binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); + arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + return 1; + } else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { + binaryInfo = targetInfo; + return 1; + } else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { + // types that need to be injected into + std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); + QString binaryPath; + + { // try to find java automatically + WCHAR buffer[MAX_PATH]; + if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { + DWORD binaryType = 0UL; + if (!::GetBinaryTypeW(buffer, &binaryType)) { + qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); + } else if (binaryType == SCS_32BIT_BINARY) { + binaryPath = ToQString(buffer); + } + } + } + if (binaryPath.isEmpty() && (extension == "jar")) { + // second attempt: look to the registry + QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); + if (javaReg.contains("CurrentVersion")) { + QString currentVersion = javaReg.value("CurrentVersion").toString(); + binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); + } + } + if (binaryPath.isEmpty()) { + binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); + } + if (binaryPath.isEmpty()) { + return 0; + } + binaryInfo = QFileInfo(binaryPath); + if (extension == "jar") { + arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } else { + arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } + return 1; + } else { + return 2; + } +} + + +void MainWindow::addAsExecutable() +{ + if (m_ContextItem != nullptr) { + QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); + QFileInfo binaryInfo; + QString arguments; + switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { + case 1: { + QString name = QInputDialog::getText(this, tr("Enter Name"), + tr("Please enter a name for the executable"), QLineEdit::Normal, + targetInfo.baseName()); + if (!name.isEmpty()) { + //Note: If this already exists, you'll lose custom settings + m_OrganizerCore.executablesList()->addExecutable(name, + binaryInfo.absoluteFilePath(), + arguments, + targetInfo.absolutePath(), + QString(), + Executable::CustomExecutable); + refreshExecutablesList(); + } + } break; + case 2: { + QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable.")); + } break; + default: { + // nop + } break; + } + } +} + + +void MainWindow::originModified(int originID) +{ + FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID); + origin.enable(false); + m_OrganizerCore.directoryStructure()->addFromOrigin(origin.getName(), origin.getPath(), origin.getPriority()); + DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure()); +} + + +void MainWindow::hideFile() +{ + QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); + QString newName = oldName + ModInfo::s_HiddenExt; + + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return; + } + } else { + return; + } + } + + if (QFile::rename(oldName, newName)) { + originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); + refreshDataTreeKeepExpandedNodes(); + } else { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(oldName).arg(QDir::toNativeSeparators(newName))); + } +} + + +void MainWindow::unhideFile() +{ + QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); + QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return; + } + } else { + return; + } + } + if (QFile::rename(oldName, newName)) { + originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); + refreshDataTreeKeepExpandedNodes(); + } else { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); + } +} + + +void MainWindow::enableSelectedPlugins_clicked() +{ + m_OrganizerCore.pluginList()->enableSelected(ui->espList->selectionModel()); +} + + +void MainWindow::disableSelectedPlugins_clicked() +{ + m_OrganizerCore.pluginList()->disableSelected(ui->espList->selectionModel()); +} + +void MainWindow::sendSelectedPluginsToTop_clicked() +{ + m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), 0); +} + +void MainWindow::sendSelectedPluginsToBottom_clicked() +{ + m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), INT_MAX); +} + +void MainWindow::sendSelectedPluginsToPriority_clicked() +{ + bool ok; + int newPriority = QInputDialog::getInt(this, + tr("Set Priority"), tr("Set the priority of the selected plugins"), + 0, 0, INT_MAX, 1, &ok); + if (!ok) return; + + m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), newPriority); +} + + +void MainWindow::enableSelectedMods_clicked() +{ + m_OrganizerCore.modList()->enableSelected(ui->modList->selectionModel()); + if (m_ModListSortProxy != nullptr) { + m_ModListSortProxy->invalidate(); + } +} + + +void MainWindow::disableSelectedMods_clicked() +{ + m_OrganizerCore.modList()->disableSelected(ui->modList->selectionModel()); + if (m_ModListSortProxy != nullptr) { + m_ModListSortProxy->invalidate(); + } +} + + +void MainWindow::previewDataFile() +{ + QString fileName = QDir::fromNativeSeparators(m_ContextItem->data(0, Qt::UserRole).toString()); + + // what we have is an absolute path to the file in its actual location (for the primary origin) + // what we want is the path relative to the virtual data directory + + // we need to look in the virtual directory for the file to make sure the info is up to date. + + // check if the file comes from the actual data folder instead of a mod + QDir gameDirectory = m_OrganizerCore.managedGame()->dataDirectory().absolutePath(); + QString relativePath = gameDirectory.relativeFilePath(fileName); + QDir dirRelativePath = gameDirectory.relativeFilePath(fileName); + // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case + if (!dirRelativePath.isAbsolute() && !relativePath.startsWith("..")) { + fileName = relativePath; + } + else { + // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory + int offset = m_OrganizerCore.settings().getModDirectory().size() + 1; + offset = fileName.indexOf("/", offset); + fileName = fileName.mid(offset + 1); + } + + + + const FileEntry::Ptr file = m_OrganizerCore.directoryStructure()->searchFile(ToWString(fileName), nullptr); + + if (file.get() == nullptr) { + reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); + return; + } + + // set up preview dialog + PreviewDialog preview(fileName); + auto addFunc = [&] (int originId) { + FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originId); + QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; + if (QFile::exists(filePath)) { + // it's very possible the file doesn't exist, because it's inside an archive. we don't support that + QWidget *wid = m_PluginContainer.previewGenerator().genPreview(filePath); + if (wid == nullptr) { + reportError(tr("failed to generate preview for %1").arg(filePath)); + } else { + preview.addVariant(ToQString(origin.getName()), wid); + } + } + }; + + addFunc(file->getOrigin()); + for (auto alt : file->getAlternatives()) { + addFunc(alt.first); + } + if (preview.numVariants() > 0) { + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(preview.objectName()); + if (settings.contains(key)) { + preview.restoreGeometry(settings.value(key).toByteArray()); + } + preview.exec(); + settings.setValue(key, preview.saveGeometry()); + } else { + QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); + } +} + +void MainWindow::openDataFile() +{ + if (m_ContextItem != nullptr) { + QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); + QFileInfo binaryInfo; + QString arguments; + switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { + case 1: { + m_OrganizerCore.spawnBinaryDirect( + binaryInfo, arguments, m_OrganizerCore.currentProfile()->name(), + targetInfo.absolutePath(), "", ""); + } break; + case 2: { + ::ShellExecuteW(nullptr, L"open", + ToWString(targetInfo.absoluteFilePath()).c_str(), + nullptr, nullptr, SW_SHOWNORMAL); + } break; + default: { + // nop + } break; + } + } +} + + +void MainWindow::updateAvailable() +{ + for (QAction *action : ui->toolBar->actions()) { + if (action->text() == tr("Update")) { + action->setEnabled(true); + action->setToolTip(tr("Update available")); + break; + } + } +} + + +void MainWindow::motdReceived(const QString &motd) +{ + // don't show motd after 5 seconds, may be annoying. Hopefully the user's + // internet connection is faster next time + if (m_StartTime.secsTo(QTime::currentTime()) < 5) { + uint hash = qHash(motd); + if (hash != m_OrganizerCore.settings().getMotDHash()) { + MotDDialog dialog(motd); + dialog.exec(); + m_OrganizerCore.settings().setMotDHash(hash); + } + } + + ui->actionEndorseMO->setVisible(false); +} + + +void MainWindow::notEndorsedYet() +{ + if (!Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { + ui->actionEndorseMO->setVisible(true); + } +} + + +void MainWindow::wontEndorse() +{ + Settings::instance().directInterface().setValue("wont_endorse_MO", true); + ui->actionEndorseMO->setVisible(false); +} + + +void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) +{ + QTreeWidget *dataTree = findChild("dataTree"); + m_ContextItem = dataTree->itemAt(pos.x(), pos.y()); + + QMenu menu; + if ((m_ContextItem != nullptr) && (m_ContextItem->childCount() == 0) + && (m_ContextItem->data(0, Qt::UserRole + 3).toBool() != true)) { + menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); + menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable())); + + QString fileName = m_ContextItem->text(0); + if (m_PluginContainer.previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { + menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); + } + + // offer to hide/unhide file, but not for files from archives + if (!m_ContextItem->data(0, Qt::UserRole + 1).toBool()) { + if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile())); + } else { + menu.addAction(tr("Hide"), this, SLOT(hideFile())); + } + } + + menu.addSeparator(); + } + menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile())); + menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked())); + + menu.exec(dataTree->mapToGlobal(pos)); +} + +void MainWindow::on_conflictsCheckBox_toggled(bool) +{ + refreshDataTreeKeepExpandedNodes(); +} + + +void MainWindow::on_actionUpdate_triggered() +{ + m_OrganizerCore.startMOUpdate(); +} + + +void MainWindow::on_actionEndorseMO_triggered() +{ + // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now + IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); + if (!game) return; + + if (QMessageBox::question(this, tr("Endorse Mod Organizer"), + tr("Do you want to endorse Mod Organizer on %1 now?").arg( + NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement( + game->gameShortName(), game->nexusModOrganizerID(), m_OrganizerCore.getVersion().canonicalString(), true, this, QVariant(), QString()); + } +} + + +void MainWindow::initDownloadView() +{ + DownloadList *sourceModel = new DownloadList(m_OrganizerCore.downloadManager(), ui->downloadView); + DownloadListSortProxy *sortProxy = new DownloadListSortProxy(m_OrganizerCore.downloadManager(), ui->downloadView); + sortProxy->setSourceModel(sourceModel); + connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), sortProxy, SLOT(updateFilter(QString))); + connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(downloadFilterChanged(QString))); + + ui->downloadView->setSourceModel(sourceModel); + ui->downloadView->setModel(sortProxy); + ui->downloadView->setManager(m_OrganizerCore.downloadManager()); + ui->downloadView->setItemDelegate(new DownloadProgressDelegate(m_OrganizerCore.downloadManager(), sortProxy, ui->downloadView)); + updateDownloadView(); + + connect(ui->downloadView, SIGNAL(installDownload(int)), &m_OrganizerCore, SLOT(installDownload(int))); + connect(ui->downloadView, SIGNAL(queryInfo(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfo(int))); + connect(ui->downloadView, SIGNAL(visitOnNexus(int)), m_OrganizerCore.downloadManager(), SLOT(visitOnNexus(int))); + connect(ui->downloadView, SIGNAL(openFile(int)), m_OrganizerCore.downloadManager(), SLOT(openFile(int))); + connect(ui->downloadView, SIGNAL(openInDownloadsFolder(int)), m_OrganizerCore.downloadManager(), SLOT(openInDownloadsFolder(int))); + connect(ui->downloadView, SIGNAL(removeDownload(int, bool)), m_OrganizerCore.downloadManager(), SLOT(removeDownload(int, bool))); + connect(ui->downloadView, SIGNAL(restoreDownload(int)), m_OrganizerCore.downloadManager(), SLOT(restoreDownload(int))); + connect(ui->downloadView, SIGNAL(cancelDownload(int)), m_OrganizerCore.downloadManager(), SLOT(cancelDownload(int))); + connect(ui->downloadView, SIGNAL(pauseDownload(int)), m_OrganizerCore.downloadManager(), SLOT(pauseDownload(int))); + connect(ui->downloadView, SIGNAL(resumeDownload(int)), this, SLOT(resumeDownload(int))); +} + +void MainWindow::updateDownloadView() +{ + // set the view attribute and default row sizes + if (m_OrganizerCore.settings().compactDownloads()) { + ui->downloadView->setProperty("downloadView", "compact"); + setStyleSheet("DownloadListWidget::item { padding: 4px 2px; }"); + } else { + ui->downloadView->setProperty("downloadView", "standard"); + setStyleSheet("DownloadListWidget::item { padding: 16px 4px; }"); + } + //setStyleSheet("DownloadListWidget::item:hover { padding: 0px; }"); + //setStyleSheet("DownloadListWidget::item:selected { padding: 0px; }"); + + // reapply global stylesheet on the widget level (!) to override the defaults + //ui->downloadView->setStyleSheet(styleSheet()); + + ui->downloadView->setMetaDisplay(m_OrganizerCore.settings().metaDownloads()); + ui->downloadView->style()->unpolish(ui->downloadView); + ui->downloadView->style()->polish(ui->downloadView); + qobject_cast(ui->downloadView->header())->customResizeSections(); + m_OrganizerCore.downloadManager()->refreshList(); +} + +void MainWindow::modDetailsUpdated(bool) +{ + if (--m_ModsToUpdate == 0) { + statusBar()->hide(); + m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } + m_RefreshProgress->setVisible(false); + } else { + m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); + } +} + +void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int) +{ + m_ModsToUpdate -= static_cast(modIDs.size()); + QVariantList resultList = resultData.toList(); + for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) { + QVariantMap result = iter->toMap(); + // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now + IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); + if (game + && result["id"].toInt() == game->nexusModOrganizerID() + && result["game_id"].toInt() == game->nexusGameID()) { + if (!result["voted_by_user"].toBool() && + Settings::instance().endorsementIntegration() && + !Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { + ui->actionEndorseMO->setVisible(true); + } + } else { + QString gameName = m_OrganizerCore.managedGame()->gameShortName(); + bool sameNexus = false; + for (IPluginGame *game : m_PluginContainer.plugins()) { + if (game->nexusGameID() == result["game_id"].toInt()) { + gameName = game->gameShortName(); + if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID()) + sameNexus = true; + break; + } + } + std::vector info = ModInfo::getByModID(gameName, result["id"].toInt()); + if (sameNexus) { + std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), result["id"].toInt()); + info.reserve(info.size() + mainInfo.size()); + info.insert(info.end(), mainInfo.begin(), mainInfo.end()); + } + for (auto iter = info.begin(); iter != info.end(); ++iter) { + (*iter)->setNewestVersion(result["version"].toString()); + (*iter)->setNexusDescription(result["description"].toString()); + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated() && + result.contains("voted_by_user") && + Settings::instance().endorsementIntegration()) { + // don't use endorsement info if we're not logged in or if the response doesn't contain it + (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); + } + } + } + } + + if (m_ModsToUpdate <= 0) { + statusBar()->hide(); + m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } + } else { + m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); + } +} + +void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int) +{ + QMap results = resultData.toMap(); + if (results["code"].toInt() == 200) { + if (results["status"].toString().compare("Endorsed") == 0) { + QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!")); + } else { + QMessageBox::information(this, tr("Okay."), tr("This mod will not be endorsed and will no longer ask you to endorse.")); + } + ui->actionEndorseMO->setVisible(false); + if (!disconnect(sender(), SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), + this, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)))) { + qCritical("failed to disconnect endorsement slot"); + } + } +} + +void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultData, int) +{ + QVariantList serverList = resultData.toList(); + + QList servers; + for (const QVariant &server : serverList) { + QVariantMap serverInfo = server.toMap(); + ServerInfo info; + info.name = serverInfo["Name"].toString(); + info.premium = serverInfo["IsPremium"].toBool(); + info.lastSeen = QDate::currentDate(); + info.preferred = !info.name.compare("CDN", Qt::CaseInsensitive); + // other keys: ConnectedUsers, Country, URI + servers.append(info); + } + m_OrganizerCore.settings().updateServers(servers); +} + + +void MainWindow::nxmRequestFailed(QString, int modID, int, QVariant, int, const QString &errorString) +{ + if (modID == -1) { + // must be the update-check that failed + m_ModsToUpdate = 0; + statusBar()->hide(); + } + MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); +} + + +BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, + QProgressDialog &progress) +{ + QDir().mkdir(destination); + BSA::EErrorCode result = BSA::ERROR_NONE; + QString errorFile; + + for (unsigned int i = 0; i < folder->getNumFiles(); ++i) { + BSA::File::Ptr file = folder->getFile(i); + BSA::EErrorCode res = archive.extract(file, qUtf8Printable(destination)); + if (res != BSA::ERROR_NONE) { + reportError(tr("failed to read %1: %2").arg(file->getName().c_str()).arg(res)); + result = res; + } + progress.setLabelText(file->getName().c_str()); + progress.setValue(progress.value() + 1); + QCoreApplication::processEvents(); + if (progress.wasCanceled()) { + result = BSA::ERROR_CANCELED; + } + } + + if (result != BSA::ERROR_NONE) { + if (QMessageBox::critical(this, tr("Error"), tr("failed to extract %1 (errorcode %2)").arg(errorFile).arg(result), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { + return result; + } + } + + for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) { + BSA::Folder::Ptr subFolder = folder->getSubFolder(i); + BSA::EErrorCode res = extractBSA(archive, subFolder, + destination.mid(0).append("/").append(subFolder->getName().c_str()), progress); + if (res != BSA::ERROR_NONE) { + return res; + } + } + return BSA::ERROR_NONE; +} + + +bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std::string fileName) +{ + progress.setLabelText(fileName.c_str()); + progress.setValue(percentage); + QCoreApplication::processEvents(); + return !progress.wasCanceled(); +} + + +void MainWindow::extractBSATriggered() +{ + QTreeWidgetItem *item = m_ContextItem; + QString origin; + + QString targetFolder = FileDialogMemory::getExistingDirectory("extractBSA", this, tr("Extract BSA")); + QStringList archives = {}; + if (!targetFolder.isEmpty()) { + if (!item->parent()) { + for (int i = 0; i < item->childCount(); ++i) { + archives.append(item->child(i)->text(0)); + } + origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(0))).getPath())); + } else { + origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(1))).getPath())); + archives = QStringList({ item->text(0) }); + } + + for (auto archiveName : archives) { + BSA::Archive archive; + QString archivePath = QString("%1\\%2").arg(origin).arg(archiveName); + 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; + } + + QProgressDialog progress(this); + progress.setMaximum(100); + progress.setValue(0); + progress.show(); + archive.extractAll(QDir::toNativeSeparators(targetFolder).toLocal8Bit().constData(), + boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2)); + if (result == BSA::ERROR_INVALIDHASHES) { + reportError(tr("This archive contains invalid hashes. Some files may be broken.")); + } + archive.close(); + } + } +} + + +void MainWindow::displayColumnSelection(const QPoint &pos) +{ + QMenu menu; + + // display a list of all headers as checkboxes + QAbstractItemModel *model = ui->modList->header()->model(); + for (int i = 1; i < model->columnCount(); ++i) { + QString columnName = model->headerData(i, Qt::Horizontal).toString(); + QCheckBox *checkBox = new QCheckBox(&menu); + checkBox->setText(columnName); + checkBox->setChecked(!ui->modList->header()->isSectionHidden(i)); + QWidgetAction *checkableAction = new QWidgetAction(&menu); + checkableAction->setDefaultWidget(checkBox); + menu.addAction(checkableAction); + } + menu.exec(pos); + + // view/hide columns depending on check-state + int i = 1; + for (const QAction *action : menu.actions()) { + const QWidgetAction *widgetAction = qobject_cast(action); + if (widgetAction != nullptr) { + const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); + if (checkBox != nullptr) { + ui->modList->header()->setSectionHidden(i, !checkBox->isChecked()); + } + } + ++i; + } +} + +void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos) +{ + m_ContextItem = ui->bsaList->itemAt(pos); + +// m_ContextRow = ui->bsaList->indexOfTopLevelItem(ui->bsaList->itemAt(pos)); + + QMenu menu; + menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered())); + + menu.exec(ui->bsaList->mapToGlobal(pos)); +} + +void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int) +{ + m_ArchiveListWriter.write(); + m_CheckBSATimer.start(500); +} + +void MainWindow::on_actionNotifications_triggered() +{ + updateProblemsButton(); + ProblemsDialog problems(m_PluginContainer.plugins(), this); + if (problems.hasProblems()) { + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(problems.objectName()); + if (settings.contains(key)) { + problems.restoreGeometry(settings.value(key).toByteArray()); + } + problems.exec(); + settings.setValue(key, problems.saveGeometry()); + updateProblemsButton(); + } +} + +void MainWindow::on_actionChange_Game_triggered() +{ + if (QMessageBox::question(this, tr("Are you sure?"), + tr("This will restart MO, continue?"), + QMessageBox::Yes | QMessageBox::Cancel) + == QMessageBox::Yes) { + InstanceManager::instance().clearCurrentInstance(); + qApp->exit(INT_MAX); + } +} + +void MainWindow::setCategoryListVisible(bool visible) +{ + if (visible) { + ui->categoriesGroup->show(); + ui->displayCategoriesBtn->setText(ToQString(L"\u00ab")); + } else { + ui->categoriesGroup->hide(); + ui->displayCategoriesBtn->setText(ToQString(L"\u00bb")); + } +} + +void MainWindow::on_displayCategoriesBtn_toggled(bool checked) +{ + setCategoryListVisible(checked); +} + +void MainWindow::editCategories() +{ + CategoriesDialog dialog(this); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog.objectName()); + if (settings.contains(key)) { + dialog.restoreGeometry(settings.value(key).toByteArray()); + } + if (dialog.exec() == QDialog::Accepted) { + dialog.commitChanges(); + } + settings.setValue(key, dialog.saveGeometry()); + +} + +void MainWindow::deselectFilters() +{ + ui->categoriesList->clearSelection(); +} + +void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos) +{ + QMenu menu; + menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories())); + menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters())); + + menu.exec(ui->categoriesList->mapToGlobal(pos)); +} + + +void MainWindow::updateESPLock(bool locked) +{ + QItemSelection currentSelection = ui->espList->selectionModel()->selection(); + if (currentSelection.count() == 0) { + // this path is probably useless + m_OrganizerCore.pluginList()->lockESPIndex(m_ContextRow, locked); + } else { + Q_FOREACH (const QModelIndex &idx, currentSelection.indexes()) { + if (m_OrganizerCore.pluginList()->isEnabled(mapToModel(m_OrganizerCore.pluginList(), idx).row())) { + m_OrganizerCore.pluginList()->lockESPIndex(mapToModel(m_OrganizerCore.pluginList(), idx).row(), locked); + } + } + } +} + + +void MainWindow::lockESPIndex() +{ + updateESPLock(true); +} + +void MainWindow::unlockESPIndex() +{ + updateESPLock(false); +} + + +void MainWindow::removeFromToolbar() +{ + try { + Executable &exe = m_OrganizerCore.executablesList()->find(m_ContextAction->text()); + exe.showOnToolbar(false); + } catch (const std::runtime_error&) { + qDebug("executable doesn't exist any more"); + } + + updateToolBar(); +} + + +void MainWindow::toolBar_customContextMenuRequested(const QPoint &point) +{ + QAction *action = ui->toolBar->actionAt(point); + if (action != nullptr) { + if (action->objectName().startsWith("custom_")) { + m_ContextAction = action; + QMenu menu; + menu.addAction(tr("Remove"), this, SLOT(removeFromToolbar())); + menu.exec(ui->toolBar->mapToGlobal(point)); + } + } +} + +void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) +{ + m_ContextRow = m_PluginListSortProxy->mapToSource(ui->espList->indexAt(pos)).row(); + + QMenu menu; + menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedPlugins_clicked())); + menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedPlugins_clicked())); + + menu.addSeparator(); + + menu.addAction(tr("Enable all"), m_OrganizerCore.pluginList(), SLOT(enableAll())); + menu.addAction(tr("Disable all"), m_OrganizerCore.pluginList(), SLOT(disableAll())); + + menu.addSeparator(); + + addPluginSendToContextMenu(&menu); + + QItemSelection currentSelection = ui->espList->selectionModel()->selection(); + bool hasLocked = false; + bool hasUnlocked = false; + for (const QModelIndex &idx : currentSelection.indexes()) { + int row = m_PluginListSortProxy->mapToSource(idx).row(); + if (m_OrganizerCore.pluginList()->isEnabled(row)) { + if (m_OrganizerCore.pluginList()->isESPLocked(row)) { + hasLocked = true; + } else { + hasUnlocked = true; + } + } + } + + if (hasLocked) { + menu.addAction(tr("Unlock load order"), this, SLOT(unlockESPIndex())); + } + if (hasUnlocked) { + menu.addAction(tr("Lock load order"), this, SLOT(lockESPIndex())); + } + + menu.addSeparator(); + + + QModelIndex idx = ui->espList->selectionModel()->currentIndex(); + unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(idx.data().toString())); + //this is to avoid showing the option on game files like skyrim.esm + if (modInfoIndex != UINT_MAX) { + menu.addAction(tr("Open Origin in Explorer"), this, SLOT(openOriginExplorer_clicked())); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); + std::vector flags = modInfo->getFlags(); + + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) { + QAction *infoAction = menu.addAction(tr("Open Origin Info..."), this, SLOT(openOriginInformation_clicked())); + menu.setDefaultAction(infoAction); + } + } + + try { + menu.exec(ui->espList->mapToGlobal(pos)); + } catch (const std::exception &e) { + reportError(tr("Exception: ").arg(e.what())); + } catch (...) { + reportError(tr("Unknown exception")); + } +} + +void MainWindow::on_groupCombo_currentIndexChanged(int index) +{ + if (m_ModListSortProxy == nullptr) { + return; + } + QAbstractProxyModel *newModel = nullptr; + switch (index) { + case 1: { + newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_CATEGORY, Qt::UserRole, + 0, Qt::UserRole + 2); + } break; + case 2: { + newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_MODID, Qt::DisplayRole, + QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE, + Qt::UserRole + 2); + } break; + default: { + newModel = nullptr; + } break; + } + + if (newModel != nullptr) { +#ifdef TEST_MODELS + new ModelTest(newModel, this); +#endif // TEST_MODELS + m_ModListSortProxy->setSourceModel(newModel); + connect(ui->modList, SIGNAL(expanded(QModelIndex)),newModel, SLOT(expanded(QModelIndex))); + connect(ui->modList, SIGNAL(collapsed(QModelIndex)), newModel, SLOT(collapsed(QModelIndex))); + connect(newModel, SIGNAL(expandItem(QModelIndex)), this, SLOT(expandModList(QModelIndex))); + } else { + m_ModListSortProxy->setSourceModel(m_OrganizerCore.modList()); + } + modFilterActive(m_ModListSortProxy->isFilterActive()); +} + +const Executable &MainWindow::getSelectedExecutable() const +{ + QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); + return m_OrganizerCore.executablesList()->find(name); +} + +Executable &MainWindow::getSelectedExecutable() +{ + QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); + return m_OrganizerCore.executablesList()->find(name); +} + +void MainWindow::on_linkButton_pressed() +{ + const Executable &selectedExecutable(getSelectedExecutable()); + + const QIcon addIcon(":/MO/gui/link"); + const QIcon removeIcon(":/MO/gui/remove"); + + const QFileInfo linkDesktopFile(getDesktopLinkfile(selectedExecutable)); + const QFileInfo linkMenuFile(getStartMenuLinkfile(selectedExecutable)); + + ui->linkButton->menu()->actions().at(static_cast(ShortcutType::Toolbar))->setIcon(selectedExecutable.isShownOnToolbar() ? removeIcon : addIcon); + ui->linkButton->menu()->actions().at(static_cast(ShortcutType::Desktop))->setIcon(linkDesktopFile.exists() ? removeIcon : addIcon); + ui->linkButton->menu()->actions().at(static_cast(ShortcutType::StartMenu))->setIcon(linkMenuFile.exists() ? removeIcon : addIcon); +} + +void MainWindow::on_showHiddenBox_toggled(bool checked) +{ + m_OrganizerCore.downloadManager()->setShowHidden(checked); +} + + +void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) +{ + SECURITY_ATTRIBUTES secAttributes; + secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); + secAttributes.bInheritHandle = TRUE; + secAttributes.lpSecurityDescriptor = nullptr; + + if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { + qCritical("failed to create stdout reroute"); + } + + if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { + qCritical("failed to correctly set up the stdout reroute"); + *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; + } +} + +std::string MainWindow::readFromPipe(HANDLE stdOutRead) +{ + static const int chunkSize = 128; + std::string result; + + char buffer[chunkSize + 1]; + buffer[chunkSize] = '\0'; + + DWORD read = 1; + while (read > 0) { + if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) { + break; + } + if (read > 0) { + result.append(buffer, read); + if (read < chunkSize) { + break; + } + } + } + return result; +} + +void MainWindow::processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog) +{ + std::vector lines; + boost::split(lines, lootOut, boost::is_any_of("\r\n")); + + std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); + std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); + + for (const std::string &line : lines) { + if (line.length() > 0) { + size_t progidx = line.find("[progress]"); + size_t erroridx = line.find("[error]"); + if (progidx != std::string::npos) { + dialog.setLabelText(line.substr(progidx + 11).c_str()); + } else if (erroridx != std::string::npos) { + qWarning("%s", line.c_str()); + errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); + } else { + std::smatch match; + if (std::regex_match(line, match, exRequires)) { + std::string modName(match[1].first, match[1].second); + std::string dependency(match[2].first, match[2].second); + m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("depends on missing \"%1\"").arg(dependency.c_str())); + } else if (std::regex_match(line, match, exIncompatible)) { + std::string modName(match[1].first, match[1].second); + std::string dependency(match[2].first, match[2].second); + m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("incompatible with \"%1\"").arg(dependency.c_str())); + } else { + qDebug("[loot] %s", line.c_str()); + } + } + } + } +} + +void MainWindow::on_bossButton_clicked() +{ + std::string reportURL; + std::string errorMessages; + + //m_OrganizerCore.currentProfile()->writeModlistNow(); + m_OrganizerCore.savePluginList(); + //Create a backup of the load orders w/ LOOT in name + //to make sure that any sorting is easily undo-able. + //Need to figure out how I want to do that. + + bool success = false; + + try { + setEnabled(false); + ON_BLOCK_EXIT([&] () { setEnabled(true); }); + QProgressDialog dialog(this); + dialog.setLabelText(tr("Please wait while LOOT is running")); + dialog.setMaximum(0); + dialog.show(); + + QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); + + QStringList parameters; + parameters << "--game" << m_OrganizerCore.managedGame()->gameShortName() + << "--gamePath" << QString("\"%1\"").arg(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()) + << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(m_OrganizerCore.profilePath()) + << "--out" << QString("\"%1\"").arg(outPath); + + if (m_DidUpdateMasterList) { + parameters << "--skipUpdateMasterlist"; + } + HANDLE stdOutWrite = INVALID_HANDLE_VALUE; + HANDLE stdOutRead = INVALID_HANDLE_VALUE; + createStdoutPipe(&stdOutRead, &stdOutWrite); + try { + m_OrganizerCore.prepareVFS(); + } catch (const UsvfsConnectorException &e) { + qDebug(e.what()); + return; + } catch (const std::exception &e) { + QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what()); + return; + } + + HANDLE loot = startBinary(QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"), + parameters.join(" "), + qApp->applicationDirPath() + "/loot", + true, + stdOutWrite); + + // we don't use the write end + ::CloseHandle(stdOutWrite); + + m_OrganizerCore.pluginList()->clearAdditionalInformation(); + + DWORD retLen; + JOBOBJECT_BASIC_PROCESS_ID_LIST info; + HANDLE processHandle = loot; + + if (loot != INVALID_HANDLE_VALUE) { + bool isJobHandle = true; + ULONG lastProcessID; + DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { + if (isJobHandle) { + if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { + if (info.NumberOfProcessIdsInList == 0) { + qDebug("no more processes in job"); + break; + } else { + if (lastProcessID != info.ProcessIdList[0]) { + lastProcessID = info.ProcessIdList[0]; + if (processHandle != loot) { + ::CloseHandle(processHandle); + } + processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); + } + } + } else { + // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there + // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running. + // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without + // the right to break out. + if (::GetLastError() != ERROR_MORE_DATA) { + isJobHandle = false; + } + } + } + + if (dialog.wasCanceled()) { + if (isJobHandle) { + ::TerminateJobObject(loot, 1); + } else { + ::TerminateProcess(loot, 1); + } + } + + // keep processing events so the app doesn't appear dead + QCoreApplication::processEvents(); + std::string lootOut = readFromPipe(stdOutRead); + processLOOTOut(lootOut, errorMessages, dialog); + + res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + } + + std::string remainder = readFromPipe(stdOutRead).c_str(); + if (remainder.length() > 0) { + processLOOTOut(remainder, errorMessages, dialog); + } + DWORD exitCode = 0UL; + ::GetExitCodeProcess(processHandle, &exitCode); + ::CloseHandle(processHandle); + if (exitCode != 0UL) { + reportError(tr("loot failed. Exit code was: %1").arg(exitCode)); + return; + } else { + success = true; + QFile outFile(outPath); + outFile.open(QIODevice::ReadOnly); + QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); + QJsonArray array = doc.array(); + for (auto iter = array.begin(); iter != array.end(); ++iter) { + QJsonObject pluginObj = (*iter).toObject(); + QJsonArray pluginMessages = pluginObj["messages"].toArray(); + for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { + QJsonObject msg = (*msgIter).toObject(); + m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), + QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); + } + if (pluginObj["dirty"].toString() == "yes") + m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), "dirty"); + } + + } + } else { + reportError(tr("failed to start loot")); + } + } catch (const std::exception &e) { + reportError(tr("failed to run loot: %1").arg(e.what())); + } + + if (errorMessages.length() > 0) { + QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occured"), errorMessages.c_str(), QMessageBox::Ok, this); + warn->setModal(false); + warn->show(); + } + + if (success) { + m_DidUpdateMasterList = true; + if (reportURL.length() > 0) { + m_IntegratedBrowser.setWindowTitle("LOOT Report"); + QString report(reportURL.c_str()); + QStringList temp = report.split("?"); + QUrl url = QUrl::fromLocalFile(temp.at(0)); + if (temp.size() > 1) { + url.setQuery(temp.at(1).toUtf8()); + } + m_IntegratedBrowser.openUrl(url); + } + m_OrganizerCore.refreshESPList(false); + m_OrganizerCore.savePluginList(); + } +} + + +const char *MainWindow::PATTERN_BACKUP_GLOB = ".????_??_??_??_??_??"; +const char *MainWindow::PATTERN_BACKUP_REGEX = "\\.(\\d\\d\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d)"; +const char *MainWindow::PATTERN_BACKUP_DATE = "yyyy_MM_dd_hh_mm_ss"; + + +bool MainWindow::createBackup(const QString &filePath, const QDateTime &time) +{ + QString outPath = filePath + "." + time.toString(PATTERN_BACKUP_DATE); + if (shellCopy(QStringList(filePath), QStringList(outPath), this)) { + QFileInfo fileInfo(filePath); + removeOldFiles(fileInfo.absolutePath(), fileInfo.fileName() + PATTERN_BACKUP_GLOB, 10, QDir::Name); + return true; + } else { + return false; + } +} + +void MainWindow::on_saveButton_clicked() +{ + m_OrganizerCore.savePluginList(); + QDateTime now = QDateTime::currentDateTime(); + if (createBackup(m_OrganizerCore.currentProfile()->getPluginsFileName(), now) + && createBackup(m_OrganizerCore.currentProfile()->getLoadOrderFileName(), now) + && createBackup(m_OrganizerCore.currentProfile()->getLockedOrderFileName(), now)) { + MessageDialog::showMessage(tr("Backup of load order created"), this); + } +} + +QString MainWindow::queryRestore(const QString &filePath) +{ + QFileInfo pluginFileInfo(filePath); + QString pattern = pluginFileInfo.fileName() + ".*"; + QFileInfoList files = pluginFileInfo.absoluteDir().entryInfoList(QStringList(pattern), QDir::Files, QDir::Name); + + SelectionDialog dialog(tr("Choose backup to restore"), this); + QRegExp exp(pluginFileInfo.fileName() + PATTERN_BACKUP_REGEX); + QRegExp exp2(pluginFileInfo.fileName() + "\\.(.*)"); + for(const QFileInfo &info : boost::adaptors::reverse(files)) { + if (exp.exactMatch(info.fileName())) { + QDateTime time = QDateTime::fromString(exp.cap(1), PATTERN_BACKUP_DATE); + dialog.addChoice(time.toString(), "", exp.cap(1)); + } else if (exp2.exactMatch(info.fileName())) { + dialog.addChoice(exp2.cap(1), "", exp2.cap(1)); + } + } + + if (dialog.numChoices() == 0) { + QMessageBox::information(this, tr("No Backups"), tr("There are no backups to restore")); + return QString(); + } + + if (dialog.exec() == QDialog::Accepted) { + return dialog.getChoiceData().toString(); + } else { + return QString(); + } +} + +void MainWindow::on_restoreButton_clicked() +{ + QString pluginName = m_OrganizerCore.currentProfile()->getPluginsFileName(); + QString choice = queryRestore(pluginName); + if (!choice.isEmpty()) { + QString loadOrderName = m_OrganizerCore.currentProfile()->getLoadOrderFileName(); + QString lockedName = m_OrganizerCore.currentProfile()->getLockedOrderFileName(); + if (!shellCopy(pluginName + "." + choice, pluginName, true, this) || + !shellCopy(loadOrderName + "." + choice, loadOrderName, true, this) || + !shellCopy(lockedName + "." + choice, lockedName, true, this)) { + QMessageBox::critical(this, tr("Restore failed"), + tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError()))); + } + m_OrganizerCore.refreshESPList(true); + } +} + +void MainWindow::on_saveModsButton_clicked() +{ + m_OrganizerCore.currentProfile()->writeModlistNow(true); + QDateTime now = QDateTime::currentDateTime(); + if (createBackup(m_OrganizerCore.currentProfile()->getModlistFileName(), now)) { + MessageDialog::showMessage(tr("Backup of modlist created"), this); + } +} + +void MainWindow::on_restoreModsButton_clicked() +{ + QString modlistName = m_OrganizerCore.currentProfile()->getModlistFileName(); + QString choice = queryRestore(modlistName); + if (!choice.isEmpty()) { + if (!shellCopy(modlistName + "." + choice, modlistName, true, this)) { + QMessageBox::critical(this, tr("Restore failed"), + tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError()))); + } + m_OrganizerCore.refreshModList(false); + } +} + +void MainWindow::on_actionCopy_Log_to_Clipboard_triggered() +{ + QStringList lines; + QAbstractItemModel *model = ui->logList->model(); + for (int i = 0; i < model->rowCount(); ++i) { + lines.append(QString("%1 [%2] %3").arg(model->index(i, 0).data().toString()) + .arg(model->index(i, 1).data(Qt::UserRole).toString()) + .arg(model->index(i, 1).data().toString())); + } + QApplication::clipboard()->setText(lines.join("\n")); +} + +void MainWindow::on_categoriesAndBtn_toggled(bool checked) +{ + if (checked) { + m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_AND); + } +} + +void MainWindow::on_categoriesOrBtn_toggled(bool checked) +{ + if (checked) { + m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_OR); + } +} + +void MainWindow::on_managedArchiveLabel_linkHovered(const QString&) +{ + QToolTip::showText(QCursor::pos(), + ui->managedArchiveLabel->toolTip()); +} + +void MainWindow::dragEnterEvent(QDragEnterEvent *event) +{ + //Accept copy or move drags to the download window. Link drags are not + //meaningful (Well, they are - we could drop a link in the download folder, + //but you need privileges to do that). + if (ui->downloadTab->isVisible() && + (event->proposedAction() == Qt::CopyAction || + event->proposedAction() == Qt::MoveAction) && + event->answerRect().intersects(ui->downloadTab->rect())) { + + //If I read the documentation right, this won't work under a motif windows + //manager and the check needs to be done at the drop. However, that means + //the user might be allowed to drop things which we can't sanely process + QMimeData const *data = event->mimeData(); + + if (data->hasUrls()) { + QStringList extensions = + m_OrganizerCore.installationManager()->getSupportedExtensions(); + + //This is probably OK - scan to see if these are moderately sane archive + //types + QList urls = data->urls(); + bool ok = true; + for (const QUrl &url : urls) { + if (url.isLocalFile()) { + QString local = url.toLocalFile(); + bool fok = false; + for (auto ext : extensions) { + if (local.endsWith(ext, Qt::CaseInsensitive)) { + fok = true; + break; + } + } + if (! fok) { + ok = false; + break; + } + } + } + if (ok) { + event->accept(); + } + } + } +} + +void MainWindow::dropLocalFile(const QUrl &url, const QString &outputDir, bool move) +{ + QFileInfo file(url.toLocalFile()); + if (!file.exists()) { + qWarning("invalid source file: %s", qUtf8Printable(file.absoluteFilePath())); + return; + } + QString target = outputDir + "/" + file.fileName(); + if (QFile::exists(target)) { + QMessageBox box(QMessageBox::Question, + file.fileName(), + tr("A file with the same name has already been downloaded. " + "What would you like to do?")); + box.addButton(tr("Overwrite"), QMessageBox::ActionRole); + box.addButton(tr("Rename new file"), QMessageBox::YesRole); + box.addButton(tr("Ignore file"), QMessageBox::RejectRole); + + box.exec(); + switch (box.buttonRole(box.clickedButton())) { + case QMessageBox::RejectRole: + return; + case QMessageBox::ActionRole: + break; + default: + case QMessageBox::YesRole: + target = m_OrganizerCore.downloadManager()->getDownloadFileName(file.fileName()); + break; + } + } + + bool success = false; + if (move) { + success = shellMove(file.absoluteFilePath(), target, true, this); + } else { + success = shellCopy(file.absoluteFilePath(), target, true, this); + } + if (!success) { + qCritical("file operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError()))); + } +} + +bool MainWindow::registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName) { + // register the view so it's geometry gets saved at exit + m_PersistedGeometry.push_back(std::make_pair(name, view)); + + // also, restore the geometry if it was saved before + QSettings &settings = m_OrganizerCore.settings().directInterface(); + + QString key = QString("geometry/%1").arg(name); + QByteArray data; + + if ((oldSettingName != nullptr) && settings.contains(oldSettingName)) { + data = settings.value(oldSettingName).toByteArray(); + settings.remove(oldSettingName); + } else if (settings.contains(key)) { + data = settings.value(key).toByteArray(); + } + + if (!data.isEmpty()) { + view->restoreState(data); + return true; + } else { + return false; + } +} + +void MainWindow::dropEvent(QDropEvent *event) +{ + Qt::DropAction action = event->proposedAction(); + QString outputDir = m_OrganizerCore.downloadManager()->getOutputDirectory(); + if (action == Qt::MoveAction) { + //Tell windows I'm taking control and will delete the source of a move. + event->setDropAction(Qt::TargetMoveAction); + } + for (const QUrl &url : event->mimeData()->urls()) { + if (url.isLocalFile()) { + dropLocalFile(url, outputDir, action == Qt::MoveAction); + } else { + m_OrganizerCore.downloadManager()->startDownloadURLs(QStringList() << url.url()); + } + } + event->accept(); +} + + +void MainWindow::on_clickBlankButton_clicked() +{ + deselectFilters(); +} + +void MainWindow::on_clearFiltersButton_clicked() +{ + ui->modFilterEdit->clear(); + deselectFilters(); +} + +void MainWindow::sendSelectedModsToPriority(int newPriority) +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + std::vector modsToMove; + for (auto idx : selection->selectedRows(ModList::COL_PRIORITY)) { + modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt())); + } + m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority); + } else { + m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority); + } +} + +void MainWindow::sendSelectedModsToTop_clicked() +{ + sendSelectedModsToPriority(0); +} + +void MainWindow::sendSelectedModsToBottom_clicked() +{ + sendSelectedModsToPriority(INT_MAX); +} + +void MainWindow::sendSelectedModsToPriority_clicked() +{ + bool ok; + int newPriority = QInputDialog::getInt(this, + tr("Set Priority"), tr("Set the priority of the selected mods"), + 0, 0, INT_MAX, 1, &ok); + if (!ok) return; + + sendSelectedModsToPriority(newPriority); +} + +void MainWindow::sendSelectedModsToSeparator_clicked() +{ + QStringList separators; + auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); + for (auto iter = indexesByPriority.begin(); iter != indexesByPriority.end(); iter++) { + if ((iter->second != UINT_MAX)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(iter->second); + if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) { + separators << modInfo->name().chopped(10); // Chops the "_separator" away from the name + } + } + } + + ListDialog dialog(this); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog.objectName()); + + dialog.setWindowTitle("Select a separator..."); + dialog.setChoices(separators); + dialog.restoreGeometry(settings.value(key).toByteArray()); + if (dialog.exec() == QDialog::Accepted) { + QString result = dialog.getChoice(); + if (!result.isEmpty()) { + result += "_separator"; + + int newPriority = INT_MAX; + bool foundSection = false; + for (auto mod : m_OrganizerCore.modsSortedByProfilePriority()) { + unsigned int modIndex = ModInfo::getIndex(mod); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if (!foundSection && result.compare(mod) == 0) { + foundSection = true; + } else if (foundSection && modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) { + newPriority = m_OrganizerCore.currentProfile()->getModPriority(modIndex); + break; + } + } + + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + std::vector modsToMove; + for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) { + modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt())); + } + m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority); + } else { + int oldPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); + if (oldPriority < newPriority) + --newPriority; + m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority); + } + } + } + settings.setValue(key, dialog.saveGeometry()); +} + +void MainWindow::on_showArchiveDataCheckBox_toggled(const bool checked) +{ + if (m_OrganizerCore.getArchiveParsing() && checked) + { + m_showArchiveData = checked; + } + else + { + m_showArchiveData = false; + } + refreshDataTree(); +} + diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 529f20cb..3523feae 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -216,8 +216,10 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re setNewestVersion(VersionInfo(result["version"].toString())); setNexusDescription(result["description"].toString()); - if ((m_EndorsedState != ENDORSED_NEVER) && (result.contains("voted_by_user"))) { - setEndorsedState(result["voted_by_user"].toBool() ? ENDORSED_TRUE : ENDORSED_FALSE); + if ((m_EndorsedState != ENDORSED_NEVER) && (result.contains("endorsement"))) { + QVariantMap endorsement = result["endorsement"].toMap(); + QString endorsementStatus = endorsement["endorse_status"].toString(); + setEndorsedState(endorsementStatus.compare("Endorsed") == 0 ? ENDORSED_TRUE : ENDORSED_FALSE); } m_LastNexusQuery = QDateTime::currentDateTime(); //m_MetaInfoChanged = true; @@ -228,10 +230,17 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData) { - m_EndorsedState = resultData.toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; - m_MetaInfoChanged = true; - saveMeta(); - emit modDetailsUpdated(true); + QMap results = resultData.toMap(); + if (results["code"].toInt() == 200 || results["code"].toInt() == 201) { + if (results["status"].toString().compare("Endorsed") == 0) { + m_EndorsedState = ENDORSED_TRUE; + } else { + m_EndorsedState = ENDORSED_FALSE; + } + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); + } } @@ -435,7 +444,7 @@ bool ModInfoRegular::remove() void ModInfoRegular::endorse(bool doEndorse) { if (doEndorse != (m_EndorsedState == ENDORSED_TRUE)) { - m_NexusBridge.requestToggleEndorsement(m_GameName, getNexusID(), doEndorse, QVariant(1)); + m_NexusBridge.requestToggleEndorsement(m_GameName, getNexusID(), m_Version.canonicalString(), doEndorse, QVariant(1)); } } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 993ae41e..4fe86136 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -28,6 +28,7 @@ along with Mod Organizer. If not, see . #include #include +#include #include @@ -62,9 +63,9 @@ void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QV m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule)); } -void NexusBridge::requestToggleEndorsement(QString gameName, int modID, bool endorse, QVariant userData) +void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData) { - m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, endorse, this, userData, m_SubModule)); + m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule)); } void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -85,17 +86,18 @@ void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userDa QList fileInfoList; - QVariantList resultList = resultData.toList(); + QVariantMap resultInfo = resultData.toMap(); + QList resultList = resultInfo["files"].toList(); for (const QVariant &file : resultList) { ModRepositoryFileInfo temp; QVariantMap fileInfo = file.toMap(); - temp.uri = fileInfo["uri"].toString(); + temp.uri = fileInfo["file_name"].toString(); temp.name = fileInfo["name"].toString(); - temp.description = fileInfo["description"].toString(); + temp.description = fileInfo["changelog_html"].toString(); temp.version = VersionInfo(fileInfo["version"].toString()); temp.categoryID = fileInfo["category_id"].toInt(); - temp.fileID = fileInfo["id"].toInt(); + temp.fileID = fileInfo["file_id"].toInt(); temp.fileSize = fileInfo["size"].toInt(); fileInfoList.append(temp); } @@ -399,10 +401,10 @@ int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, } -int NexusInterface::requestToggleEndorsement(QString gameName, int modID, bool endorse, QObject *receiver, QVariant userData, +int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); + NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); requestInfo.m_Endorse = endorse; m_RequestQueue.enqueue(requestInfo); @@ -456,11 +458,11 @@ void NexusInterface::nextRequest() return; } - if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->loggedIn()) { - if (!getAccessManager()->loginAttempted()) { + if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { + if (!getAccessManager()->validateAttempted()) { emit needLogin(); return; - } else if (getAccessManager()->loginWaiting()) { + } else if (getAccessManager()->validateWaiting()) { return; } } @@ -469,42 +471,51 @@ void NexusInterface::nextRequest() info.m_Timeout = new QTimer(this); info.m_Timeout->setInterval(60000); + QJsonObject postObject; + QJsonDocument postData(postObject); + QString url; if (!info.m_Reroute) { bool hasParams = false; switch (info.m_Type) { case NXMRequestInfo::TYPE_DESCRIPTION: { - url = QString("%1/Mods/%2/").arg(info.m_URL).arg(info.m_ModID); + url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); } break; case NXMRequestInfo::TYPE_FILES: { - url = QString("%1/Files/indexfrommod/%2/").arg(info.m_URL).arg(info.m_ModID); + url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); } break; case NXMRequestInfo::TYPE_FILEINFO: { - url = QString("%1/Files/%2/").arg(info.m_URL).arg(info.m_FileID); + url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); } break; case NXMRequestInfo::TYPE_DOWNLOADURL: { - url = QString("%1/Files/download/%2").arg(info.m_URL).arg(info.m_FileID); + url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); } break; case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { - url = QString("%1/Mods/toggleendorsement/%2?lvote=%3").arg(info.m_URL).arg(info.m_ModID).arg(!info.m_Endorse); - hasParams = true; + QString endorse = info.m_Endorse ? "endorse" : "abstain"; + url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse); + postObject.insert("Version", info.m_ModVersion); + postData.setObject(postObject); } break; case NXMRequestInfo::TYPE_GETUPDATES: { QString modIDList = VectorJoin(info.m_ModIDList, ","); modIDList = "[" + modIDList + "]"; url = QString("%1/Mods/GetUpdates?ModList=%2").arg(info.m_URL).arg(modIDList); - hasParams = true; } break; } - url.append(QString("%1game_id=%2").arg(hasParams ? '&' : '?').arg(info.m_NexusGameID)); } else { url = info.m_URL; } QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); - request.setRawHeader("User-Agent", m_AccessManager->userAgent(info.m_SubModule).toUtf8()); + request.setRawHeader("apikey", m_AccessManager->apiKey().toUtf8()); + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule)); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Protocol-Version", "0.5.5"); + request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8()); - info.m_Reply = m_AccessManager->get(request); + if (postData.object().isEmpty()) + info.m_Reply = m_AccessManager->get(request); + else + info.m_Reply = m_AccessManager->post(request, postData.toJson()); connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); @@ -627,7 +638,7 @@ void NexusInterface::requestTimeout() namespace { QString get_management_url(MOBase::IPluginGame const *game) { - return "https://legacy-api.nexusmods.com/" + game->gameNexusName().toLower(); + return "https://api.nexusmods.com/v1"; } } @@ -638,6 +649,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , MOBase::IPluginGame const *game ) : m_ModID(modID) + , m_ModVersion("0") , m_FileID(0) , m_Reply(nullptr) , m_Type(type) @@ -648,7 +660,30 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_URL(get_management_url(game)) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameShortName()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , QString modVersion + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(modID) + , m_ModVersion(modVersion) + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) , m_Endorse(false) {} @@ -659,6 +694,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector modIDList , MOBase::IPluginGame const *game ) : m_ModID(-1) + , m_ModVersion("0") , m_ModIDList(modIDList) , m_FileID(0) , m_Reply(nullptr) @@ -670,18 +706,19 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector modIDList , m_URL(get_management_url(game)) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameShortName()) + , m_GameName(game->gameNexusName()) , m_Endorse(false) {} NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID - , int fileID - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game - ) + , int fileID + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) : m_ModID(modID) + , m_ModVersion("0") , m_FileID(fileID) , m_Reply(nullptr) , m_Type(type) @@ -692,6 +729,6 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_URL(get_management_url(game)) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameShortName()) + , m_GameName(game->gameNexusName()) , m_Endorse(false) {} diff --git a/src/nexusinterface.h b/src/nexusinterface.h index defb370f..7b707711 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -96,7 +96,7 @@ public: * @param modID id of the mod caller is interested in * @param userData user data to be returned with the result */ - virtual void requestToggleEndorsement(QString gameName, int modID, bool endorse, QVariant userData); + virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData); public slots: @@ -255,9 +255,9 @@ public: * @param userData user data to be returned with the result * @return int an id to identify the request */ - int requestToggleEndorsement(QString gameName, int modID, bool endorse, QObject *receiver, QVariant userData, const QString &subModule) + int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule) { - return requestToggleEndorsement(gameName, modID, endorse, receiver, userData, subModule, getGame(gameName)); + return requestToggleEndorsement(gameName, modID, modVersion, endorse, receiver, userData, subModule, getGame(gameName)); } /** @@ -269,7 +269,7 @@ public: * @param game the game with which the mods are associated * @return int an id to identify the request */ - int requestToggleEndorsement(QString gameName, int modID, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, + int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); /** @@ -363,6 +363,7 @@ private: struct NXMRequestInfo { int m_ModID; + QString m_ModVersion; std::vector m_ModIDList; int m_FileID; QNetworkReply *m_Reply; @@ -385,6 +386,7 @@ private: int m_Endorse; NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(std::vector modIDList, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 912eab30..dc1eb2cb 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -1,351 +1,264 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "nxmaccessmanager.h" - -#include "iplugingame.h" -#include "nxmurl.h" -#include "report.h" -#include "utility.h" -#include "selfupdater.h" -#include "persistentcookiejar.h" -#include "settings.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace MOBase; - -namespace { - QString const Nexus_Management_URL("https://legacy-api.nexusmods.com"); -} - -// unfortunately Nexus doesn't seem to document these states, all I know is all these listed -// are considered premium (27 should be lifetime premium) -const std::set NXMAccessManager::s_PremiumAccountStates { 4, 6, 13, 27, 31, 32 }; - - -NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) - : QNetworkAccessManager(parent) - , m_LoginReply(nullptr) - , m_MOVersion(moVersion) -{ - m_LoginTimeout.setSingleShot(true); - m_LoginTimeout.setInterval(30000); - setCookieJar(new PersistentCookieJar( - QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat"))); - - if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) { - // why is this necessary all of a sudden? - setNetworkAccessible(QNetworkAccessManager::Accessible); - } -} - -NXMAccessManager::~NXMAccessManager() -{ - if (m_LoginReply != nullptr) { - m_LoginReply->deleteLater(); - m_LoginReply = nullptr; - } -} - -void NXMAccessManager::setNMMVersion(const QString &nmmVersion) -{ - m_NMMVersion = nmmVersion; -} - -QNetworkReply *NXMAccessManager::createRequest( - QNetworkAccessManager::Operation operation, const QNetworkRequest &request, - QIODevice *device) -{ - if (request.url().scheme() != "nxm") { - return QNetworkAccessManager::createRequest(operation, request, device); - } - if (operation == GetOperation) { - emit requestNXMDownload(request.url().toString()); - - // eat the request, everything else will be done by the download manager - return QNetworkAccessManager::createRequest(QNetworkAccessManager::GetOperation, - QNetworkRequest(QUrl())); - } else if (operation == PostOperation) { - return QNetworkAccessManager::createRequest(operation, request, device);; - } else { - return QNetworkAccessManager::createRequest(operation, request, device); - } -} - - -void NXMAccessManager::showCookies() const -{ - QUrl url(Nexus_Management_URL + "/"); - for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) { - qDebug("%s - %s (expires: %s)", - cookie.name().constData(), cookie.value().constData(), - qUtf8Printable(cookie.expirationDate().toString())); - } -} - -void NXMAccessManager::clearCookies() -{ - PersistentCookieJar *jar = qobject_cast(cookieJar()); - if (jar != nullptr) { - jar->clear(); - } else { - qWarning("failed to clear cookies, invalid cookie jar"); - } -} - -void NXMAccessManager::startLoginCheck() -{ - if (hasLoginCookies()) { - qDebug("validating login cookies"); - QNetworkRequest request(Nexus_Management_URL + "/Sessions/?Validate"); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - request.setRawHeader("User-Agent", userAgent().toUtf8()); - - m_LoginReply = get(request); - m_LoginTimeout.start(); - m_LoginState = LOGIN_CHECKING; - connect(m_LoginReply, SIGNAL(finished()), this, SLOT(loginChecked())); - connect(m_LoginReply, SIGNAL(error(QNetworkReply::NetworkError)), - this, SLOT(loginError(QNetworkReply::NetworkError))); - } -} - - -void NXMAccessManager::retrieveCredentials() -{ - qDebug("retrieving credentials"); - - QNetworkRequest request(Nexus_Management_URL + "/Core/Libs/Flamework/Entities/User?GetCredentials"); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - request.setRawHeader("User-Agent", userAgent().toUtf8()); - - QNetworkReply *reply = get(request); - QTimer timeout; - connect(&timeout, &QTimer::timeout, [reply] () { - reply->deleteLater(); - }); - timeout.start(); - - connect(reply, &QNetworkReply::finished, [reply, this] () { - QJsonDocument jdoc = QJsonDocument::fromJson(reply->readAll()); - QJsonArray credentialsData = jdoc.array(); - emit credentialsReceived(credentialsData.at(2).toString(), - s_PremiumAccountStates.find(credentialsData.at(1).toInt()) - != s_PremiumAccountStates.end()); - reply->deleteLater(); - }); - - connect(reply, static_cast(&QNetworkReply::error), - [=] (QNetworkReply::NetworkError) { - qDebug("failed to retrieve account credentials: %s", qUtf8Printable(reply->errorString())); - reply->deleteLater(); - }); -} - - -bool NXMAccessManager::loggedIn() const -{ - if (m_LoginState == LOGIN_CHECKING) { - QProgressDialog progress; - progress.setLabelText(tr("Verifying Nexus login")); - progress.show(); - while (m_LoginState == LOGIN_CHECKING) { - QCoreApplication::processEvents(); - QThread::msleep(100); - } - progress.hide(); - } - - return m_LoginState == LOGIN_VALID; -} - - -void NXMAccessManager::refuseLogin() -{ - m_LoginState = LOGIN_REFUSED; -} - - -bool NXMAccessManager::loginAttempted() const -{ - return m_LoginState != LOGIN_NOT_CHECKED; -} - - -bool NXMAccessManager::loginWaiting() const -{ - return m_LoginReply != nullptr; -} - - -void NXMAccessManager::login(const QString &username, const QString &password) -{ - if (m_LoginReply != nullptr) { - return; - } - - if (m_LoginState == LOGIN_VALID) { - emit loginSuccessful(false); - return; - } - m_Username = username; - m_Password = password; - pageLogin(); -} - - -QString NXMAccessManager::userAgent(const QString &subModule) const -{ - QStringList comments; - comments << "Nexus Client v" + m_NMMVersion; - if (!subModule.isEmpty()) { - comments << "module: " + subModule; - } - - return QString("Mod Organizer/%1 (%2)").arg(m_MOVersion, comments.join("; ")); -} - - -void NXMAccessManager::pageLogin() -{ - qDebug("logging %s in on Nexus", qUtf8Printable(m_Username)); - QString requestString = (Nexus_Management_URL + "/Sessions/?Login&uri=%1") - .arg(QString(QUrl::toPercentEncoding(Nexus_Management_URL))); - - QNetworkRequest request(requestString); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - QByteArray postDataQuery; - QUrlQuery postData; - postData.addQueryItem("username", m_Username); - postData.addQueryItem("password", QUrl::toPercentEncoding(m_Password)); - postDataQuery = postData.query(QUrl::EncodeReserved).toUtf8(); - - request.setRawHeader("User-Agent", userAgent().toUtf8()); - - m_ProgressDialog = new QProgressDialog(nullptr); - m_ProgressDialog->setLabelText(tr("Logging into Nexus")); - QList buttons = m_ProgressDialog->findChildren(); - buttons.at(0)->setEnabled(false); - m_ProgressDialog->show(); - QCoreApplication::processEvents(); // for some reason the whole app hangs during the login. This way the user has at least a little feedback - - m_LoginReply = post(request, postDataQuery); - m_LoginTimeout.start(); - m_LoginState = LOGIN_CHECKING; - connect(m_LoginReply, SIGNAL(finished()), this, SLOT(loginFinished())); - connect(m_LoginReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(loginError(QNetworkReply::NetworkError))); -} - - -void NXMAccessManager::loginTimeout() -{ - m_LoginReply->deleteLater(); - m_LoginReply = nullptr; - m_LoginAttempted = false; // this usually means we might have success later - m_Username.clear(); - m_Password.clear(); - m_LoginState = LOGIN_NOT_VALID; - - emit loginFailed(tr("timeout")); -} - - -void NXMAccessManager::loginError(QNetworkReply::NetworkError) -{ - qDebug("login error"); - if (m_ProgressDialog != nullptr) { - m_ProgressDialog->deleteLater(); - m_ProgressDialog = nullptr; - } - m_Username.clear(); - m_Password.clear(); - m_LoginState = LOGIN_NOT_VALID; - - if (m_LoginReply != nullptr) { - emit loginFailed(m_LoginReply->errorString()); - m_LoginReply->deleteLater(); - m_LoginReply = nullptr; - } else { - emit loginFailed(tr("Unknown error")); - } -} - - -bool NXMAccessManager::hasLoginCookies() const -{ - QUrl url(Nexus_Management_URL + "/"); - QList cookies = cookieJar()->cookiesForUrl(url); - for (const QNetworkCookie &cookie : cookies) { - if (cookie.name() == "sid") { - return true; - } - } - return false; -} - - -void NXMAccessManager::loginFinished() -{ - if (m_ProgressDialog != nullptr) { - m_ProgressDialog->deleteLater(); - m_ProgressDialog = nullptr; - } - - m_LoginReply->deleteLater(); - m_LoginReply = nullptr; - m_Username.clear(); - m_Password.clear(); - - if (hasLoginCookies()) { - m_LoginState = LOGIN_VALID; - retrieveCredentials(); - emit loginSuccessful(true); - } else { - m_LoginState = LOGIN_NOT_VALID; - emit loginFailed(tr("Please check your password")); - } -} - - -void NXMAccessManager::loginChecked() -{ - QNetworkReply *reply = static_cast(sender()); - QByteArray data = reply->readAll(); - m_LoginState = data == "null" ? LOGIN_NOT_VALID - : LOGIN_VALID; - if (m_LoginState == LOGIN_VALID) { - retrieveCredentials(); - } else { - qDebug("cookies seem to be invalid"); - } - m_LoginReply->deleteLater(); - m_LoginReply = nullptr; -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "nxmaccessmanager.h" + +#include "iplugingame.h" +#include "nxmurl.h" +#include "report.h" +#include "utility.h" +#include "selfupdater.h" +#include "persistentcookiejar.h" +#include "settings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MOBase; + +namespace { + QString const nexusBaseUrl("https://api.nexusmods.com/v1"); +} + +NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) + : QNetworkAccessManager(parent) + , m_ValidateReply(nullptr) + , m_MOVersion(moVersion) +{ + m_ValidateTimeout.setSingleShot(true); + m_ValidateTimeout.setInterval(30000); + setCookieJar(new PersistentCookieJar( + QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat"))); + + if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) { + // why is this necessary all of a sudden? + setNetworkAccessible(QNetworkAccessManager::Accessible); + } +} + +NXMAccessManager::~NXMAccessManager() +{ + if (m_ValidateReply != nullptr) { + m_ValidateReply->deleteLater(); + m_ValidateReply = nullptr; + } +} + +void NXMAccessManager::setNMMVersion(const QString &nmmVersion) +{ + m_NMMVersion = nmmVersion; +} + +QNetworkReply *NXMAccessManager::createRequest( + QNetworkAccessManager::Operation operation, const QNetworkRequest &request, + QIODevice *device) +{ + if (request.url().scheme() != "nxm") { + return QNetworkAccessManager::createRequest(operation, request, device); + } + if (operation == GetOperation) { + emit requestNXMDownload(request.url().toString()); + + // eat the request, everything else will be done by the download manager + return QNetworkAccessManager::createRequest(QNetworkAccessManager::GetOperation, + QNetworkRequest(QUrl())); + } else if (operation == PostOperation) { + return QNetworkAccessManager::createRequest(operation, request, device);; + } else { + return QNetworkAccessManager::createRequest(operation, request, device); + } +} + + +void NXMAccessManager::showCookies() const +{ + QUrl url(nexusBaseUrl + "/"); + for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) { + qDebug("%s - %s (expires: %s)", + cookie.name().constData(), cookie.value().constData(), + qUtf8Printable(cookie.expirationDate().toString())); + } +} + +void NXMAccessManager::clearCookies() +{ + PersistentCookieJar *jar = qobject_cast(cookieJar()); + if (jar != nullptr) { + jar->clear(); + } else { + qWarning("failed to clear cookies, invalid cookie jar"); + } +} + +void NXMAccessManager::startValidationCheck() +{ + qDebug("Checking Nexus API Key..."); + QString requestString = nexusBaseUrl + "/users/validate"; + + QNetworkRequest request(requestString); + request.setRawHeader("APIKEY", m_ApiKey.toUtf8()); + request.setRawHeader("User-Agent", userAgent().toUtf8()); + + m_ProgressDialog = new QProgressDialog(nullptr); + m_ProgressDialog->setLabelText(tr("Validating Nexus Connection")); + QList buttons = m_ProgressDialog->findChildren(); + buttons.at(0)->setEnabled(false); + m_ProgressDialog->show(); + QCoreApplication::processEvents(); // for some reason the whole app hangs during the login. This way the user has at least a little feedback + + m_ValidateReply = get(request); + m_ValidateTimeout.start(); + m_ValidateState = VALIDATE_CHECKING; + connect(m_ValidateReply, SIGNAL(finished()), this, SLOT(validateFinished())); + connect(m_ValidateReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(validateError(QNetworkReply::NetworkError))); +} + + +bool NXMAccessManager::validated() const +{ + if (m_ValidateState == VALIDATE_CHECKING) { + QProgressDialog progress; + progress.setLabelText(tr("Verifying Nexus login")); + progress.show(); + while (m_ValidateState == VALIDATE_CHECKING) { + QCoreApplication::processEvents(); + QThread::msleep(100); + } + progress.hide(); + } + + return m_ValidateState == VALIDATE_VALID; +} + + +void NXMAccessManager::refuseValidation() +{ + m_ValidateState = VALIDATE_REFUSED; +} + + +bool NXMAccessManager::validateAttempted() const +{ + return m_ValidateState != VALIDATE_NOT_CHECKED; +} + + +bool NXMAccessManager::validateWaiting() const +{ + return m_ValidateReply != nullptr; +} + + +void NXMAccessManager::apiCheck(const QString &apiKey) +{ + if (m_ValidateReply != nullptr) { + return; + } + + if (m_ValidateState == VALIDATE_VALID) { + emit validateSuccessful(false); + return; + } + m_ApiKey = apiKey; + startValidationCheck(); +} + + +QString NXMAccessManager::userAgent(const QString &subModule) const +{ + QStringList comments; + comments << "Nexus Client v" + m_NMMVersion; + if (!subModule.isEmpty()) { + comments << "module: " + subModule; + } + + return QString("Mod Organizer/%1 (%2)").arg(m_MOVersion, comments.join("; ")); +} + + +QString NXMAccessManager::apiKey() const +{ + return m_ApiKey; +} + + +void NXMAccessManager::validateTimeout() +{ + m_ValidateReply->deleteLater(); + m_ValidateReply = nullptr; + m_ValidateAttempted = false; // this usually means we might have success later + m_ApiKey.clear(); + m_ValidateState = VALIDATE_NOT_VALID; + + emit validateFailed(tr("timeout")); +} + + +void NXMAccessManager::validateError(QNetworkReply::NetworkError) +{ + qDebug("login error"); + if (m_ProgressDialog != nullptr) { + m_ProgressDialog->deleteLater(); + m_ProgressDialog = nullptr; + } + m_ApiKey.clear(); + m_ValidateState = VALIDATE_NOT_VALID; + + if (m_ValidateReply != nullptr) { + emit validateFailed(m_ValidateReply->errorString()); + m_ValidateReply->deleteLater(); + m_ValidateReply = nullptr; + } else { + emit validateFailed(tr("Unknown error")); + } +} + + +void NXMAccessManager::validateFinished() +{ + if (m_ProgressDialog != nullptr) { + m_ProgressDialog->deleteLater(); + m_ProgressDialog = nullptr; + } + + if (m_ValidateReply != nullptr) { + QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll()); + QJsonObject credentialsData = jdoc.object(); + QString test = jdoc.toJson(); + QString name = credentialsData.value("name").toString(); + bool premium = credentialsData.value("is_premium?").toBool(); + emit credentialsReceived(credentialsData.value("name").toString(), + credentialsData.value("is_premium?").toBool()); + + m_ValidateReply->deleteLater(); + m_ValidateReply = nullptr; + + m_ValidateState = VALIDATE_VALID; + emit validateSuccessful(true); + } +} diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index c58c4cc3..b316ef77 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -43,12 +43,12 @@ public: void setNMMVersion(const QString &nmmVersion); - bool loggedIn() const; + bool validated() const; - bool loginAttempted() const; - bool loginWaiting() const; + bool validateAttempted() const; + bool validateWaiting() const; - void login(const QString &username, const QString &password); + void apiCheck(const QString &apiKey); void showCookies() const; @@ -56,9 +56,11 @@ public: QString userAgent(const QString &subModule = QString()) const; - void startLoginCheck(); + QString apiKey() const; - void refuseLogin(); + void startValidationCheck(); + + void refuseValidation(); signals: @@ -74,18 +76,17 @@ signals: * * @param necessary true if a login was necessary and succeeded, false if the user is still logged in **/ - void loginSuccessful(bool necessary); + void validateSuccessful(bool necessary); - void loginFailed(const QString &message); + void validateFailed(const QString &message); void credentialsReceived(const QString &userName, bool premium); private slots: - void loginChecked(); - void loginFinished(); - void loginError(QNetworkReply::NetworkError errorCode); - void loginTimeout(); + void validateFinished(); + void validateError(QNetworkReply::NetworkError errorCode); + void validateTimeout(); protected: @@ -95,38 +96,24 @@ protected: private: - void pageLogin(); -// void dlLogin(); - - bool hasLoginCookies() const; - - void retrieveCredentials(); - -private: - - static const std::set s_PremiumAccountStates; - -private: - - QTimer m_LoginTimeout; - QNetworkReply *m_LoginReply; + QTimer m_ValidateTimeout; + QNetworkReply *m_ValidateReply; QProgressDialog *m_ProgressDialog { nullptr }; QString m_MOVersion; QString m_NMMVersion; - QString m_Username; - QString m_Password; + QString m_ApiKey; - bool m_LoginAttempted; + bool m_ValidateAttempted; enum { - LOGIN_NOT_CHECKED, - LOGIN_CHECKING, - LOGIN_NOT_VALID, - LOGIN_ATTEMPT_FAILED, - LOGIN_REFUSED, - LOGIN_VALID - } m_LoginState = LOGIN_NOT_CHECKED; + VALIDATE_NOT_CHECKED, + VALIDATE_CHECKING, + VALIDATE_NOT_VALID, + VALIDATE_ATTEMPT_FAILED, + VALIDATE_REFUSED, + VALIDATE_VALID + } m_ValidateState = VALIDATE_NOT_CHECKED; }; diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 50301bfa..43c01310 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -26,7 +26,7 @@ - Lead Developers/ Maintainers + Current Maintainers @@ -35,92 +35,67 @@ - - Mo2 devs and Contributors + + Major Contributors - - Project579 - - - - - przester - - - - + Translators - + Cyb3r (Dutch) - + fruttyx (French) - + Yoplala (French) - + Faron (German) - + Mordan (Greek) - + Yoosk (Polish) - + Brgodfx (Portuguese) - - zDas (Portuguese) - - - - + Jax (Swedish) - - yohru (Japanese) - - - - + ...and all other Transifex contributors! - + Other Supporters && Contributors - - Tannin (Original Creator) - - - - + Close @@ -309,7 +284,7 @@ p, li { white-space: pre-wrap; } - + failed to read mod (%1): %2 @@ -317,251 +292,435 @@ p, li { white-space: pre-wrap; } DownloadList - + Name - - Size + + Filetime - - Status + + Size - - Filetime + + Done - - < game %1 mod %2 file %3 > + + Information missing, please select "Query Info" from the context menu to re-retrieve. - Unknown + pending download + + + DownloadListWidget - - Pending + + + Placeholder - - Started + + + + Done - Double Click to install - - Canceling + + + Paused - Double Click to resume - - Pausing + + + Installed - Double Click to re-install - - Canceled + + + Uninstalled - Double Click to re-install + + + DownloadListWidgetCompact - - Paused + + + Placeholder - - Error + + Done + + + DownloadListWidgetCompactDelegate - - Fetching Info 1 + + < game %1 mod %2 file %3 > - - Fetching Info 2 + + Pending - - Downloaded + + Paused - + + Fetching Info 1 + + + + + Fetching Info 2 + + + + Installed - + Uninstalled - - Pending download + + Done - - Information missing, please select "Query Info" from the context menu to re-retrieve. + + + + + + + + Are you sure? + + + + + This will permanently delete the selected download. + + + + + This will remove all finished downloads from this list and from disk. + + + + + This will remove all installed downloads from this list and from disk. + + + + + This will remove all uninstalled downloads from this list and from disk. - - - DownloadListWidget - + + This will permanently remove all finished downloads from this list (but NOT from disk). + + + + + This will permanently remove all installed downloads from this list (but NOT from disk). + + + + + This will permanently remove all uninstalled downloads from this list (but NOT from disk). + + + + Install - + Query Info - + Visit on Nexus - + Open File - - - + + + Show in Folder - - + Delete - + Un-Hide - + Hide - + Cancel - + Pause - + + Remove + + + + Resume - + Delete Installed... - + Delete Uninstalled... - + Delete All... - + Hide Installed... - + Hide Uninstalled... - + Hide All... - + Un-Hide All... + + + DownloadListWidgetDelegate - - - - + + < game %1 mod %2 file %3 > + + + + + Pending + + + + + Fetching Info 1 + + + + + Fetching Info 2 + + + + + + + Delete Files? - + This will permanently delete the selected download. - + This will remove all finished downloads from this list and from disk. - + This will remove all installed downloads from this list and from disk. - + This will remove all uninstalled downloads from this list and from disk. - - - + + + Are you sure? - + This will remove all finished downloads from this list (but NOT from disk). - + This will remove all installed downloads from this list (but NOT from disk). - + This will remove all uninstalled downloads from this list (but NOT from disk). + + + Install + + + + + Query Info + + + + + Visit on Nexus + + + + + Open File + + + + + + + Show in Folder + + + + + + Delete + + + + + Un-Hide + + + + + Hide + + + + + Cancel + + + + + Pause + + + + + Resume + + + + + Delete Installed... + + + + + Delete Uninstalled... + + + + + Delete All... + + + + + Hide Installed... + + + + + Hide Uninstalled... + + + + + Hide All... + + + + + Un-Hide All... + + DownloadManager @@ -587,7 +746,7 @@ p, li { white-space: pre-wrap; } - A file with the same name "%1" has already been downloaded. Do you want to download it again? The new file will receive a different name. + A file with the same name has already been downloaded. Do you want to download it again? The new file will receive a different name. @@ -618,7 +777,7 @@ p, li { white-space: pre-wrap; } - + remove: invalid download index %1 @@ -638,234 +797,234 @@ p, li { white-space: pre-wrap; } - + cancel: invalid download index %1 - + pause: invalid download index %1 - + resume: invalid download index %1 - + resume (int): invalid download index %1 - + No known download urls. Sorry, this download can't be resumed. - + query: invalid download index %1 - + Please enter the nexus mod id - + Mod ID: - + Please select the source game code for %1 - + VisitNexus: invalid download index %1 - + Nexus ID for this Mod is unknown - + OpenFile: invalid download index %1 - + OpenFileInDownloadsFolder: invalid download index %1 - + get pending: invalid download index %1 - + get path: invalid download index %1 - + Main - + Update - + Optional - + Old - + Misc - + Unknown - + display name: invalid download index %1 - + file name: invalid download index %1 - + file time: invalid download index %1 - + file size: invalid download index %1 - + progress: invalid download index %1 - + state: invalid download index %1 - + infocomplete: invalid download index %1 - - + + mod id: invalid download index %1 - + ishidden: invalid download index %1 - + file info: invalid download index %1 - + mark installed: invalid download index %1 - + mark uninstalled: invalid download index %1 - + Memory allocation error (in processing progress event). - + Memory allocation error (in processing downloaded data). - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. - + failed to re-open %1 - + Unable to write download to drive (return %1). Check the drive's available storage. @@ -984,107 +1143,92 @@ Right now the only case I know of where this needs to be overwritten is for the - - If this is enabled, the configured libraries will be automatically loaded when this executable is launched. - - - - - Force Load Libraries (*) - - - - - Configure Libraries - - - - + Use Application's Icon for shortcuts - + (*) This setting is profile-specific - - + + Add an executable - - + + Add - - + + Remove the selected executable - + Remove - + Close - + Select a binary - + Executable (%1) - + Java (32-bit) required - + MO requires 32-bit java to run this application. If you already have it installed, select javaw.exe from that installation as the binary. - + Select a directory - + Confirm - + Really remove "%1" from executables? - + Modify - - + + Save Changes? - - + + You made changes to the current executable, do you want to save them? @@ -1136,85 +1280,6 @@ Right now the only case I know of where this needs to be overwritten is for the - - ForcedLoadDialog - - - Forced Load Settings - - - - - - Adds a row to the table. - - - - - Add Row - - - - - - Deletes the selected row from the table. - - - - - Delete Row - - - - - ForcedLoadDialogWidget - - - - If checked, the specified library will be force loaded for the specified process. - - - - - - The name of the process that should be forced to load a library. - - - - - Process name - - - - - - Browse for a process. - - - - - - ... - - - - - - The path to the library to be loaded. This may be a path relative to the managed game's directory or may be an absolute path to somewhere else. - - - - - Library to load - - - - - - Browse for a library. - - - InstallDialog @@ -1280,116 +1345,103 @@ p, li { white-space: pre-wrap; } InstallationManager - + Password required - + Password - - + + Extracting files - + failed to create backup - + Mod Name - + Name - + Invalid name - + The name you entered is invalid, please enter a different one. - + File format "%1" not supported - + None of the available installer plugins were able to handle that archive. This is likely due to a corrupted or incompatible download or unrecognized archive format. - + no error - + 7z.dll not found - + 7z.dll isn't valid - + archive not found - + failed to open archive - + unsupported archive type - + internal library error - + archive invalid - + unknown archive error - - ListDialog - - - Select an item... - - - - - Filter - - - LockedDialog @@ -1447,17 +1499,17 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + failed to write to %1 - + file not found: %1 - + Save @@ -1482,47 +1534,47 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MainWindow - + Categories - + Clear - + If checked, only mods that match all selected categories are displayed. - + And - + If checked, all mods that match at least one of the selected categories are displayed. - + Or - + Profile - + Pick a module collection - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1532,84 +1584,76 @@ p, li { white-space: pre-wrap; } - + Open list options... - + Refresh list. This is usually not necessary unless you modified data outside the program. - + Show Open Folders menu... - - + + Restore Backup... - - - + + Create Backup - - - Active: - - - - - This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter. - - - - + List of available mods. - + This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders. - - - - + Filter - + Clear all Filters - + No groups - + Nexus IDs - + + + + Namefilter + + + + Pick a program to run. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1619,12 +1663,12 @@ p, li { white-space: pre-wrap; } - + Run program - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1633,17 +1677,17 @@ p, li { white-space: pre-wrap; } - + Run - + Create a shortcut in your start menu or on the desktop to the specified program - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1652,32 +1696,27 @@ p, li { white-space: pre-wrap; } - + Shortcut - + Plugins - + Sort - This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter. - - - - List of available esp/esm files - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1686,27 +1725,27 @@ p, li { white-space: pre-wrap; } - + Archives - + <html><head/><body><p>BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. <p>Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).</p><p>If there is a matching plugin, the game will load them no matter what.</p></body></html> - + <html><head/><body><p>Currently detected archives. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">What is an archive?</span></a>)</p></body></html> - + List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order. - + BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded. By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored! @@ -1714,1151 +1753,967 @@ p, li { white-space: pre-wrap; } - + Data - + refresh data-directory overview - + Refresh the overview. This may take a moment. - - - - + + + Refresh - + This is an overview of your data directory as visible to the game (and tools). - + File - + Mod - - - Filters the above list so that only conflicts are displayed. + + + Filter the above list so that only conflicts are displayed. - + Show only conflicts - - - Filters the above list so that files from archives are not shown - - - - - Show files from Archives - - - - + Saves - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This is a list of all save games for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This is a list of all savegames for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If you click &quot;Fix Mods...&quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!</span></p></body></html> - + Downloads - - Refresh downloads view - - - - + This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here. - + Show Hidden - + Tool Bar - + Install Mod - + Install &Mod - + Install a new mod from an archive - + Ctrl+M - + Profiles - + &Profiles - + Configure Profiles - + Ctrl+P - + Executables - + &Executables - + Configure the executables that can be started through Mod Organizer - + Ctrl+E - - + + Tools - + &Tools - + Ctrl+I - + Settings - + &Settings - + Configure settings and workarounds - + Ctrl+S - + Nexus - + Search nexus network for more mods - + Ctrl+N - - + + Update - + Mod Organizer is up-to-date - - - No Notifications + + + No Problems - - This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them. + + This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them. + +!Work in progress! +Right now this has very limited functionality - - + + Help - + Ctrl+H - + Endorse MO - - + + Endorse Mod Organizer - + Copy Log to Clipboard - + Change Game - + Open the Instance selection dialog to manage a different Game - + Toolbar - + Desktop - + Start Menu - + There is no supported sort mechanism for this game. You will probably have to use a third-party tool. - - Crash on exit + + Problems - - MO crashed while exiting. Some settings may not be saved. - -Error: %1 - - - - - Notifications - - - - - There are notifications to read - - - - - There are no notifications - - - - - - - Endorse + + There are potential problems with your setup - - Won't Endorse + + Everything seems to be in order - + Help on UI - - Documentation - - - - - Chat on Discord + + Documentation Wiki - + Report Issue - + Tutorials - + About - + About Qt - + Name - + Please enter a name for the new profile - + failed to create profile: %1 - + Show tutorial? - + You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the "Help"-menu. - + Downloads in progress - + There are still downloads in progress, do you really want to quit? - + Plugin "%1" failed: %2 - + Plugin "%1" failed - + Browse Mod Page - + Also in: <br> - + No conflict - + <Edit...> - + This bsa is enabled in the ini file so it may be required! - + Activating Network Proxy - - Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues. + + Notice: Your current MO version (%1) is lower than the previous version (%2).<br>The GUI may not downgrade gracefully, so you may experience oddities.<br>However, there should be no serious issues. - + Choose Mod - + Mod Archive - + Start Tutorial? - + You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue? - + failed to spawn notepad.exe: %1 - + failed to change origin name: %1 - + failed to move "%1" from mod "%2" to "%3": %4 - + <Contains %1> - + <Checked> - + <Unchecked> - + <Update> - - <Mod Backup> - - - - + <Managed by MO> - + <Managed outside MO> - + <No category> - + <Conflicted> - + <Not Endorsed> - + failed to rename mod: %1 - + Overwrite? - + This will replace the existing mod "%1". Continue? - + failed to remove mod "%1" - - - + + + failed to rename "%1" to "%2" - - - - + + + + Confirm - + Remove the following mods?<br><ul>%1</ul> - + failed to remove mod: %1 - - - + + Failed - + Installation file no longer exists - + Mods installed with old versions of MO can't be reinstalled in this way. - - Failed to create backup. - - - - + You need to be logged in with Nexus to resume a download - - - - + + You need to be logged in with Nexus to endorse - - - Endorsing multiple mods will take a while. Please wait... - - - - - - Unendorsing multiple mods will take a while. Please wait... - - - - + Failed to display overwrite dialog: %1 - - Opening Nexus Links - - - - - You are trying to open %1 links to Nexus Mods. Are you sure you want to do this? - - - - + Nexus ID for this Mod is unknown - - Opening Web Pages - - - - - You are trying to open %1 Web Pages. Are you sure you want to do this? - - - - + Web page for this mod is unknown - - <table cellspacing="5"><tr><th>Type</th><th>All</th><th>Visible</th><tr><td>Enabled mods:&emsp;</td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr><tr><td>Unmanaged/DLCs:&emsp;</td><td align=right>%5</td><td align=right>%6</td></tr><tr><td>Mod backups:&emsp;</td><td align=right>%7</td><td align=right>%8</td></tr><tr><td>Separators:&emsp;</td><td align=right>%9</td><td align=right>%10</td></tr></table> - - - - - <table cellspacing="6"><tr><th>Type</th><th>Active </th><th>Total</th></tr><tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr><tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr><tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr><tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr></table> - - - - - - + + + Create Mod... - + This will create an empty mod. Please enter a name: - - + + A mod with this name already exists - - Create Separator... - - - - - This will create a new separator. -Please enter a name: - - - - - A separator with this name already exists - - - - + This will move all files from overwrite into a new, regular mod. Please enter a name: - - Move successful. - - - - - + + Are you sure? - + About to recursively delete: - + Not logged in, endorsement information will be wrong - + Continue? - + The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated. - - + + Sorry - + I don't know a versioning scheme where %1 is newer than %2. - + Really enable all visible mods? - + Really disable all visible mods? - + Export to csv - + CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead. - + Select what mods you want export: - + All installed mods - + Only active (checked) mods from your current profile - + All currently visible mods in the mod list - + Choose what Columns to export: - + Mod_Priority - + Mod_Name - - Notes_column - - - - + Mod_Status - + Primary_Category - + Nexus_ID - + Mod_Nexus_URL - + Mod_Version - + Install_Date - + Download_File_Name - + export failed: %1 - + Open Game folder - + Open MyGames folder - - Open INIs folder - - - - + Open Instance folder - + Open Mods folder - + Open Profile folder - + Open Downloads folder - + Open MO2 Install folder - + Open MO2 Plugins folder - + Open MO2 Logs folder - + Install Mod... - + Create empty mod - - Create Separator - - - - + Enable all visible - + Disable all visible - + Check all for update - + Export to csv... - - - Send to - - - - - - Top - - - - - - Bottom - - - - - - Priority... - - - - - Separator... - - - - + All Mods - + Sync to Mods... - - Move content to Mod... - - - - + Clear Overwrite... - - + + Open in Explorer - + Restore Backup - + Remove Backup... - - + Change Categories - - + Primary Category - - Rename Separator... - - - - - Remove Separator... - - - - - Select Color... - - - - - Reset Color - - - - + Change versioning scheme - + Un-ignore update - + Ignore update - - + + Enable selected - - + + Disable selected - + Rename Mod... - + Reinstall Mod - + Remove Mod... - + Un-Endorse - + + + Endorse + + + + Won't endorse - + Endorsement state unknown - + Ignore missing data - + Mark as converted/working - + Visit on Nexus - + Visit web page - + Information... - - + + Exception: - - + + Unknown exception - + <All> - + <Multiple> - + %1 more - + Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin. @@ -2866,12 +2721,12 @@ You can also use online editors and converters instead. - + Enable Mods... - + Delete %n save(s) @@ -2879,367 +2734,328 @@ You can also use online editors and converters instead. - + failed to remove %1 - + failed to create %1 - - Restarting MO - - - - - Changing the managed game directory requires restarting MO. -Any pending downloads will be paused. - -Click OK to restart MO now. - - - - + Can't change download directory while downloads are in progress! - + failed to write to file %1 - + %1 written - + Select binary - + Binary - + Enter Name - + Please enter a name for the executable - + Not an executable - + This is not a recognized executable. - - + + Replace file? - + There already is a hidden version of this file. Replace it? - - + + File operation failed - - + + Failed to remove "%1". Maybe you lack the required file permissions? - + There already is a visible version of this file. Replace it? - - - Set Priority - - - - - Set the priority of the selected plugins - - - - + file not found: %1 - + failed to generate preview for %1 - + Sorry, can't preview anything. This function currently does not support extracting from bsas. - + Update available - + Open/Execute - + Add as Executable - + Preview - + Un-Hide - + Hide - + Write To File... - + Do you want to endorse Mod Organizer on %1 now? - + Thank you! - + Thank you for your endorsement! - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - - + + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + This will restart MO, continue? - + Edit Categories... - + Deselect filter - + Remove - + Enable all - + Disable all - + Unlock load order - + Lock load order - - Open Origin in Explorer - - - - - Open Origin Info... - - - - + depends on missing "%1" - + incompatible with "%1" - + Please wait while LOOT is running - + loot failed. Exit code was: %1 - + failed to start loot - + failed to run loot: %1 - + Errors occured - + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of modlist created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file - - - Set the priority of the selected mods - - MessageDialog - - + + Placeholder @@ -3247,82 +3063,72 @@ Click OK to restart MO now. ModInfo - + Plugins - + Textures - + Meshes - + Bethesda Archive - + UI Changes - + Sound Effects - + Scripts - + Script Extender - - Script Extender Files - - - - + SkyProc Tools - + MCM Data - + INI files - - ModGroup files - - - - - invalid content type: %1 + + invalid content type %1 - - invalid mod index: %1 + + invalid mod index %1 - + remove: invalid mod index %1 @@ -3592,56 +3398,37 @@ p, li { white-space: pre-wrap; } - + about:blank - + Endorse - - Web page URL (only used if invalid NexusID) : - - - - + Notes - - - - Enter comments about the mod here. These are displayed in the notes column of the mod list. - - - - - - - Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column. - - - - + Filetree - + Open Mod in Explorer - + A directory view of this mod - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -3651,288 +3438,288 @@ p, li { white-space: pre-wrap; } - + Previous - + Next - + Close - + &Delete - + &Rename - + &Hide - + &Unhide - + &Open - + &New Folder - - + + Save changes? - - + + Save changes to "%1"? - + File Exists - + A file with that name exists, please enter a new one - + failed to move file - + failed to create directory "optional" - - + + Info requested, please wait - + Main - + Update - + Optional - + Old - + Misc - + Unknown - + Current Version: %1 - + No update available - + (description incomplete, please visit nexus) - + <a href="%1">Visit on Nexus</a> - + Failed to delete %1 - - - - + + + + Confirm - - + + Are sure you want to delete "%1"? - - + + Are sure you want to delete the selected files? - - + + New Folder - + Failed to create "%1" - - + + Replace file? - + There already is a hidden version of this file. Replace it? - - + + File operation failed - - + + Failed to remove "%1". Maybe you lack the required file permissions? - - + + failed to rename %1 to %2 - + There already is a visible version of this file. Replace it? - + Select binary - + Binary - + file not found: %1 - + failed to generate preview for %1 - + Sorry - + Sorry, can't preview anything. This function currently does not support extracting from bsas. - + Un-Hide - + Hide - - + + Open/Execute - - + + Preview - + Name - + Please enter a name - - + + Error - + Invalid name. Must be a valid file name - + A tweak by that name exists - + Create Tweak @@ -3971,328 +3758,300 @@ p, li { white-space: pre-wrap; } ModInfoRegular - - + + failed to write %1/meta.ini: error %2 - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> - - ModInfoSeparator - - - This is a Separator - - - ModList - + Game Plugins (ESP/ESM/ESL) - + Interface - + Meshes - + Bethesda Archive - + Scripts (Papyrus) - + Script Extender Plugin - + SkyProc Patcher - + Sound or Music - + Textures - + MCM Configuration - + INI files - - ModGroup files - - - - + This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit) - + Backup - - Separator - - - - + No valid game data - + Not endorsed yet - + Overwrites loose files - + Overwritten loose files - + Loose files Overwrites & Overwritten - + Redundant - + Overwrites an archive with loose files - + Archive is overwritten by loose files - + Overwrites another archive file - + Overwritten by another archive file - + Archive files overwrites & overwritten - - <br>This mod is for a different game, make sure it's compatible or it could cause crashes. + + Alternate game source - + Non-MO - + invalid - + installed version: "%1", newest version: "%2" - + The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade". - + Categories: <br> - + Invalid name - + Name is already in use by another mod - + drag&drop failed: %1 - + Confirm - + Are you sure you want to remove "%1"? - + Flags - + Content - + Mod Name - + Version - + Priority - + Category - + Source Game - + Nexus ID - + Installation - - Notes - - - - - + + unknown - + Name of your mods - + Version of the mod (if available) - + Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority. - + Category of the mod. - + The source game which was the origin of this mod. - + Id of the mod as used on Nexus. - + Emblemes to highlight things that might require attention. - - Depicts the content of the mod:<br><table cellspacing=7><tr><td><img src=":/MO/gui/content/plugin" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr><tr><td><img src=":/MO/gui/content/interface" width=32/></td><td>Interface</td></tr><tr><td><img src=":/MO/gui/content/mesh" width=32/></td><td>Meshes</td></tr><tr><td><img src=":/MO/gui/content/bsa" width=32/></td><td>BSA</td></tr><tr><td><img src=":/MO/gui/content/texture" width=32/></td><td>Textures</td></tr><tr><td><img src=":/MO/gui/content/sound" width=32/></td><td>Sounds</td></tr><tr><td><img src=":/MO/gui/content/music" width=32/></td><td>Music</td></tr><tr><td><img src=":/MO/gui/content/string" width=32/></td><td>Strings</td></tr><tr><td><img src=":/MO/gui/content/script" width=32/></td><td>Scripts (Papyrus)</td></tr><tr><td><img src=":/MO/gui/content/skse" width=32/></td><td>Script Extender plugins</td></tr><tr><td><img src=":/MO/gui/content/skyproc" width=32/></td><td>SkyProc Patcher</td></tr><tr><td><img src=":/MO/gui/content/menu" width=32/></td><td>Mod Configuration Menu</td></tr><tr><td><img src=":/MO/gui/content/inifile" width=32/></td><td>INI files</td></tr><tr><td><img src=":/MO/gui/content/modgroup" width=32/></td><td>ModGroup files</td></tr></table> + + Depicts the content of the mod:<br><table cellspacing=7><tr><td><img src=":/MO/gui/content/plugin" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr><tr><td><img src=":/MO/gui/content/interface" width=32/></td><td>Interface</td></tr><tr><td><img src=":/MO/gui/content/mesh" width=32/></td><td>Meshes</td></tr><tr><td><img src=":/MO/gui/content/bsa" width=32/></td><td>BSA</td></tr><tr><td><img src=":/MO/gui/content/texture" width=32/></td><td>Textures</td></tr><tr><td><img src=":/MO/gui/content/sound" width=32/></td><td>Sounds</td></tr><tr><td><img src=":/MO/gui/content/music" width=32/></td><td>Music</td></tr><tr><td><img src=":/MO/gui/content/string" width=32/></td><td>Strings</td></tr><tr><td><img src=":/MO/gui/content/script" width=32/></td><td>Scripts (Papyrus)</td></tr><tr><td><img src=":/MO/gui/content/skse" width=32/></td><td>Script Extender plugins</td></tr><tr><td><img src=":/MO/gui/content/skyproc" width=32/></td><td>SkyProc Patcher</td></tr><tr><td><img src=":/MO/gui/content/menu" width=32/></td><td>Mod Configuration Menu</td></tr><tr><td><img src=":/MO/gui/content/inifile" width=32/></td><td>INI files</td></tr></table> - + Time this mod was installed - - - User notes about the mod - - ModListSortProxy - + Drag&Drop is only supported when sorting by priority @@ -4326,30 +4085,25 @@ p, li { white-space: pre-wrap; } NXMAccessManager - - Verifying Nexus login + + Validating Nexus Connection - - Logging into Nexus + + Verifying Nexus login - + timeout - + Unknown error - - - Please check your password - - NXMUrl @@ -4362,17 +4116,17 @@ p, li { white-space: pre-wrap; } NexusInterface - + Failed to guess mod id for "%1", please pick the correct one - + empty response - + invalid response @@ -4380,222 +4134,201 @@ p, li { white-space: pre-wrap; } OrganizerCore - - + + Failed to write settings - + An error occured trying to update MO settings to %1: %2 - + File is write protected - + Invalid file format (probably a bug) - + Unknown error %1 - + An error occured trying to write back MO settings to %1: %2 - - + + Download started - + Download failed - - + + Installation successful - - + + Configure Mod - - + + This mod contains ini tweaks. Do you want to configure them now? - - - mod not found: %1 - - - - + - - - Installation cancelled + mod "%1" not found - - - Another installation is currently in progress. + + + Installation cancelled - - + + The mod was not installed completely. - - Executable not found: %1 + + Executable "%1" not found - + Start Steam? - + Steam is required to be running already to correctly start the game. Should MO try to start steam now? - - Error - - - - - Windows Event Log Error - - - - - The Windows Event Log service is disabled and/or not running. This prevents USVFS from running properly. Your mods may not be working in the executable that you are launching. Note that you may have to restart MO and/or your PC after the service is fixed. - -Continue launching %1? + + Error - - Blacklisted Executable + + Windows Event Log Error - - The executable you are attempted to launch is blacklisted in the virtual file system. This will likely prevent the executable, and any executables that are launched by this one, from seeing any mods. This could extend to INI files, save games and any other virtualized files. + + The Windows Event Log service is disabled and/or not running. This prevents USVFS from running properly. Your mods may not be working in the executable that you are launching. Note that you may have to restart MO and/or your PC after the service is fixed. Continue launching %1? - + No profile set - + Failed to refresh list of esps: %1 - + Multiple esps/esls activated, please check that they don't conflict. - + Download? - + A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue? - - + failed to update mod list: %1 - - + + login successful - + Login failed - + Login failed, try again? - + login failed: %1. Download will not be associated with an account - + login failed: %1 - + login failed: %1. You need to log-in with Nexus to update MO. - + MO1 "Script Extender" load mechanism has left hook.dll in your game folder - - + + Description missing - + <a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn. - + failed to save load order: %1 - + The designated write target "%1" is not enabled. @@ -4639,7 +4372,7 @@ Continue? - mod not found: %1 + %1 not found @@ -4701,135 +4434,135 @@ Continue? PluginList - + Name - + Priority - + Mod Index - + Flags - - + + unknown - + Name of your mods - + Load priority of your mod. The higher, the more "important" it is and thus overwrites data from plugins with lower priority. - + The modindex determines the formids of objects originating from this mods. - + failed to update esp info for file %1 (source id: %2), error: %3 - - Plugin not found: %1 + + esp not found: %1 - - + + Confirm - + Really enable all plugins? - + Really disable all plugins? - + The file containing locked plugin indices is broken - - + + <b>Origin</b>: %1 - + <br><b><i>This plugin can't be disabled (enforced by the game).</i></b> - + Author - + Description - + Missing Masters - + Enabled Masters - + Loads Archives - + There are Archives connected to this plugin. Their assets will be added to your game, overwriting in case of conflicts following the plugin order. Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin) - + Loads INI settings - + There is an ini file connected to this plugin. Its settings will be added to your game settings, overwriting in case of conflicts. - + This ESP is flagged as an ESL. It will adhere to the ESP load order but the records will be loaded in ESL space. - + failed to restore load order for %1 @@ -4837,7 +4570,7 @@ Continue? PluginListSortProxy - + Drag&Drop is only supported when sorting by priority or mod index @@ -4859,7 +4592,7 @@ Continue? ProblemsDialog - Notifications + Problems @@ -4892,7 +4625,7 @@ p, li { white-space: pre-wrap; } Profile - invalid profile name: %1 + invalid profile name %1 @@ -4901,82 +4634,57 @@ p, li { white-space: pre-wrap; } - + failed to write mod list: %1 - + failed to update tweaked ini file, wrong settings may be used: %1 - + failed to create tweaked ini: %1 - + failed to open %1 - + "%1" is missing or inaccessible - - - - - - - - invalid mod index: %1 + + + + + + invalid index %1 - + Overwrite directory couldn't be parsed - + invalid priority %1 - - Delete profile-specific save games? - - - - - Do you want to delete the profile-specific save games? (If you select "No", the save games will show up again if you re-enable profile-specific save games) - - - - - Missing profile-specific game INI files! - - - - - Some of your profile-specific game INI files were missing. They will now be copied from the vanilla game folder. You might want double-check your settings. - -Missing files: - - - - - - Delete profile-specific game INI files? + + Delete savegames? - - Do you want to delete the profile-specific game INI files? (If you select "No", the INI files will be used again if you re-enable profile-specific game INI files.) + + Do you want to delete local savegames? (If you select "No", the save games will show up again if you re-enable local savegames) @@ -4994,17 +4702,17 @@ Missing files: - If checked, the new profile will use the default game INI settings. + If checked, the new profile will use the default game settings. - If checked, the new profile will use the default game INI settings instead of the "global" settings. Global settings are the settings you configure when running the game launcher directly, without MO. + If checked, the new profile will use the default game settings instead of the "global" settings. Global settings are the settings you configure when running the game launcher directly, without MO. - Default Game INI Settings + Default Game Settings @@ -5033,41 +4741,27 @@ p, li { white-space: pre-wrap; } - <html><head/><body><p>If checked, save games are stored locally to this profile and will not appear when starting with a different profile.</p></body></html> - - - - If checked, save games are local to this profile and will not appear when starting with a different profile. + If checked, savegames are local to this profile and will not appear when starting with a different profile. - Use profile-specific Save Games + Local Savegames - <html><head/><body><p>If checked MO2 will use his own profile-specific game INI files, so that the &quot;Global&quot; ones in MyGames can be left vanilla. This different set of INI files is then offered to the game instead of the default one.</p></body></html> - - - - - <html><head/><body><p>If checked, MO2 will use a local set of game INI files (configuration and settings files), different from the default ones found in MyGames. This way changes to the INI settings will only affect this profile and the Global INI files can remain vanilla. MO2 will then show the profile INI files to the game instead of the Global ones.</p></body></html> - - - - - Use profile-specific Game INI Files + Local Game Settings - + This ensures data files from mods are actually used. You want to enable this unless you use a different tool for Archive Invalidation. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -5079,65 +4773,65 @@ p, li { white-space: pre-wrap; } - + Automatic Archive Invalidation - - + + Create a new profile from scratch - + Create - + Clone the selected profile - + This creates a new profile with the same settings and active mods as the selected one. - + Copy - - + + Delete the selected Profile. This can not be un-done! - + Remove - + Rename - - + + Transfer save games to the selected profile. - + Transfer Saves - + Close @@ -5194,7 +4888,7 @@ p, li { white-space: pre-wrap; } - Are you sure you want to remove this profile (including profile-specific save games, if any)? + Are you sure you want to remove this profile (including local savegames if any)? @@ -5228,32 +4922,13 @@ p, li { white-space: pre-wrap; } - - QApplication - - - INI file is read-only - - - - - - Mod Organizer is attempting to write to "%1" which is currently set to read-only. Clear the read-only flag to allow the write? - - - - - File is read-only - - - QObject - + Error @@ -5299,12 +4974,12 @@ p, li { white-space: pre-wrap; } - invalid category index: %1 + invalid index %1 - invalid category id: %1 + invalid category id %1 @@ -5353,13 +5028,12 @@ p, li { white-space: pre-wrap; } - - + helper failed - + failed to determine account name @@ -5370,142 +5044,140 @@ p, li { white-space: pre-wrap; } - + archive.dll not loaded: "%1" - + Deleting folder - + I'm about to delete the following folder: "%1". Proceed? - + Choose Instance to Delete - + Be Carefull! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched. - + Are you sure? - + Are you really sure you want to delete the Instance "%1" with all its files? - + Failed to delete Instance - + Could not delete Instance "%1". If the folder was still in use, restart MO and try again. - + Enter a Name for the new Instance - - Enter a new name or select one from the suggested list: -(This is just a name for the Instance and can be whatever you wish, - the actual game selection will happen on the next screen regardless of chosen name) + + Enter a new name or select one from the suggested list: - - + + Canceled - + Invalid instance name - + The instance name "%1" is invalid. Use the name "%2" instead? - + The instance "%1" already exists. - + Please choose a different instance name, like: "%1 1" . - + Choose Instance - + Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder). - + New - + Create a new instance. - + Portable - + Use MO folder for data. - + Manage Instances - + Delete an Instance. - - + + failed to create %1 - + Data directory created - + New data directory created at %1. If you don't want to store a lot of data there, reconfigure the storage directories via settings. @@ -5517,7 +5189,7 @@ If the folder was still in use, restart MO and try again. - file not found: %1 + %1 not found @@ -5575,7 +5247,7 @@ If the folder was still in use, restart MO and try again. - + Failed to create "%1". Your user account probably lacks permission. @@ -5592,64 +5264,58 @@ If the folder was still in use, restart MO and try again. - Please select the game to manage - - Canceled finding game in "%1". - - - - - No game identified in "%1". The directory is required to contain the game binary. + + No game identified in "%1". The directory is required to contain the game binary and its launcher. - + Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!) - + failed to start shortcut: %1 - + failed to start application: %1 - - + + Mod Organizer - + An instance of Mod Organizer is already running - + Failed to set up instance - + Please use "Help" from the toolbar to get usage instructions to all elements - - + + <Manage...> - + failed to parse profile %1: %2 @@ -5685,12 +5351,12 @@ If the folder was still in use, restart MO and try again. - + failed to access %1 - + failed to set file time %1 @@ -5700,39 +5366,37 @@ If the folder was still in use, restart MO and try again. - + Script Extender - + Proxy DLL - + failed to spawn "%1" - + Elevation required - + This process requires elevation to run. This is a potential security risk so I highly advice you to investigate if "%1" can be installed to work without elevation. -Restart Mod Organizer as an elevated process? -You will be asked if you want to allow helper.exe to make changes to the system. You will need to relaunch the process above manually. +Start elevated anyway? (you will be asked if you want to allow ModOrganizer.exe to make changes to the system) - - + failed to spawn "%1": %2 @@ -5898,12 +5562,12 @@ Select Show Details option to see the full change-log. - + Failed to start %1: %2 - + Error @@ -5911,42 +5575,31 @@ Select Show Details option to see the full change-log. Settings - + Failed - + Sorry, failed to start the helper application - - + + attempt to store setting for unknown plugin "%1" - + Error - + Failed to create "%1", you may not have the necessary permission. path remains unchanged. - - - Restart Mod Organizer? - - - - - In order to reset the window geometries, MO must be restarted. -Restart it now? - - SettingsDialog @@ -6020,188 +5673,136 @@ If you use pre-releases, never contact me directly by e-mail or via private mess - Colors - - - - - - When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section. - - - - - Show mod list separator colors on the scrollbar - - - - - Plugin is Contained in selected Mod - - - - - Is overwritten (loose files) - - - - - Is overwriting (loose files) - - - - - Reset Colors - - - - - Mod Contains selected Plugin - - - - - Is overwritten (archive files) + If checked, the download interface will be more compact. - - Is overwriting (archive files) + + Compact Download Interface - - - Modify the categories available to arrange your mods. + + If checked, the download list will display meta information instead of file names. - - Configure Mod Categories + + Download Meta Information - + Reset stored information from dialogs. - + This will make all dialogs show up again where you checked the "Remember selection"-box. - + Reset Dialogs - - If checked, the download interface will be more compact. - - - - - Compact Download Interface - - - - - If checked, the download list will display meta information instead of file names. + + + Modify the categories available to arrange your mods. - - Download Meta Information + + Configure Mod Categories - + Paths - - - - + + + ... - + Caches - + Overwrite - - + + Directory where downloads are stored. - + Downloads - + Profiles - + Directory where mods are stored. - + Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don't exist in the new location (with the same name). - + Mods - + Managed Game - + Base Directory - + Use %BASE_DIR% to refer to the Base Directory. - + Important: All directories have to be writeable! - - + + Nexus - + Allows automatic log-in when the Nexus-Page for the game is clicked. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -6210,149 +5811,137 @@ p, li { white-space: pre-wrap; } - - If checked and if correct credentials are entered below, log-in to Nexus (for browsing and downloading) is automatic. - - - - - Automatically Log-In to Nexus + + Connect to Nexus - - - Username - - - - - - Password - - - - + Remove cache and cookies. Forces a new login. - + Clear Cache - + Disable automatic internet features - + Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) - + Offline Mode - + Use a proxy for network connections. - + Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. - + Use HTTP Proxy (Uses System Settings) - - Endorsement Integration - - - - + Associate with "Download with manager" links - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + + Username + + + + + Password + + + + If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure. - + Plugins - + Author: - + Version: - + Description: - + Key - + Value - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -6368,17 +5957,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -6389,17 +5978,17 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - + NMM Version - + The Version of Nexus Mod Manager to impersonate. - + Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in. On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn't need an update. Therefore you can configure the version to identify as here. Please note that MO does identify itself as MO to the webserver, it's not lying about what it is. It is merely adding a "compatible" NMM version to the user agent. @@ -6408,178 +5997,127 @@ tl;dr-version: If Nexus-features don't work, insert the current version num - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - - Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - - - - - By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. -However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. - -If you disable this feature, MO will only display official DLCs this way. Please note that plugins (esps, esms, and esls) displayed in the right pane are completely unaffected by this feature. - - - - - Display mods installed outside MO - - - - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - - - Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - - - - - Lock GUI when running executable - - - - - Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness. + + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - - <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html> + + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. +However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. + +If you disable this feature, MO will only display official DLCs this way. Please note that plugins (esps, esms, and esls) displayed in the right pane are completely unaffected by this feature. - - Enable parsing of Archives (Experimental Feature) + + Display mods installed outside MO - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - - Add executables to the blacklist to prevent them from -accessing the virtual file system. This is useful to prevent -unintended programs from being hooked. Hooking unintended -programs may affect the execution of these programs or the -programs you are intentionally running. - - - - - Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running. - - - - - Configure Executables Blacklist - - - - - - Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations. + + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - - Reset Window Geometries + + Diagnostics - - These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. + + Log Level - - Diagnostics + + Decides the amount of data printed to "ModOrganizer.log" - - Max Dumps To Keep + + + Decides the amount of data printed to "ModOrganizer.log". + "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. + - - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. + + Debug - - - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - Set "Crash Dumps" above to None to disable crash dump collection. - + + Info (recommended) - - Hint: right click link and copy link location + + Warning - - - Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> - and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. - Sending logs and/or crash dumps to the developers can help investigate issues. - It is recommended to compress large log and dmp files before sending. - + + Error - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -6590,131 +6128,105 @@ programs you are intentionally running. - + None - + Mini (recommended) - + Data - + Full - - Log Level + + Max Dumps To Keep - - Decides the amount of data printed to "ModOrganizer.log" + + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + - Decides the amount of data printed to "ModOrganizer.log". - "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. + Set "Crash Dumps" above to None to disable crash dump collection. - - Debug - - - - - Info (recommended) - - - - - Warning + + Hint: right click link and copy link location - - Error + + + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> + and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. + Sending logs and/or crash dumps to the developers can help investigate issues. + It is recommended to compress large log and dmp files before sending. + - + Confirm - + Changing the mod directory affects all your profiles! Mods not present (or named differently) in the new location will be disabled in all profiles. There is no way to undo this unless you backed up your profiles manually. Proceed? - - Executables Blacklist - - - - - Enter one executable per line to be blacklisted from the virtual file system. -Mods and other virtualized files will not be visible to these executables and -any executables launched by them. - -Example: - Chrome.exe - Firefox.exe - - - - + Select base directory - + Select download directory - + Select mod directory - + Select cache directory - + Select profiles directory - + Select overwrite directory - - Select game executable - - - - + Confirm? - + This will make all dialogs show up again where you checked the "Remember selection"-box. Continue? @@ -6826,7 +6338,7 @@ Example: TransferSavesDialog - Transfer Save Games + Transfer Savegames @@ -6918,7 +6430,7 @@ On Windows XP: UsvfsConnector - + Preparing vfs diff --git a/src/organizercore.cpp b/src/organizercore.cpp index a668de6b..dca855c7 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1,2485 +1,2461 @@ -#include "organizercore.h" - -#include "delayedfilewriter.h" -#include "guessedvalue.h" -#include "imodinterface.h" -#include "imoinfo.h" -#include "iplugingame.h" -#include "iuserinterface.h" -#include "loadmechanism.h" -#include "messagedialog.h" -#include "modlistsortproxy.h" -#include "modrepositoryfileinfo.h" -#include "nexusinterface.h" -#include "plugincontainer.h" -#include "pluginlistsortproxy.h" -#include "profile.h" -#include "logbuffer.h" -#include "credentialsdialog.h" -#include "filedialogmemory.h" -#include "modinfodialog.h" -#include "spawn.h" -#include "syncoverwritedialog.h" -#include "nxmaccessmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include "appconfig.h" -#include -#include -#include "lockeddialog.h" -#include "instancemanager.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include // for qUtf8Printable, etc - -#include -#include -#include -#include // for _tcsicmp - -#include -#include -#include // for memset, wcsrchr - -#include -#include -#include -#include -#include -#include //for wstring -#include -#include - - -using namespace MOShared; -using namespace MOBase; - -//static -CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None; - -static bool isOnline() -{ - QList interfaces = QNetworkInterface::allInterfaces(); - - bool connected = false; - for (auto iter = interfaces.begin(); iter != interfaces.end() && !connected; - ++iter) { - if ((iter->flags() & QNetworkInterface::IsUp) - && (iter->flags() & QNetworkInterface::IsRunning) - && !(iter->flags() & QNetworkInterface::IsLoopBack)) { - auto addresses = iter->addressEntries(); - if (addresses.count() == 0) { - continue; - } - qDebug("interface %s seems to be up (address: %s)", - qUtf8Printable(iter->humanReadableName()), - qUtf8Printable(addresses[0].ip().toString())); - connected = true; - } - } - - return connected; -} - -static bool renameFile(const QString &oldName, const QString &newName, - bool overwrite = true) -{ - if (overwrite && QFile::exists(newName)) { - QFile::remove(newName); - } - return QFile::rename(oldName, newName); -} - -static std::wstring getProcessName(HANDLE process) -{ - wchar_t buffer[MAX_PATH]; - wchar_t *fileName = L"unknown"; - - if (process == nullptr) return fileName; - - if (::GetProcessImageFileNameW(process, buffer, MAX_PATH) != 0) { - fileName = wcsrchr(buffer, L'\\'); - if (fileName == nullptr) { - fileName = buffer; - } - else { - fileName += 1; - } - } - - return fileName; -} - -// Get parent PID for the given process, return 0 on failure -static DWORD getProcessParentID(DWORD pid) -{ - HANDLE th = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - PROCESSENTRY32 pe = { 0 }; - pe.dwSize = sizeof(PROCESSENTRY32); - - DWORD res = 0; - if (Process32First(th, &pe)) - do { - if (pe.th32ProcessID == pid) { - res = pe.th32ParentProcessID; - break; - } - } while (Process32Next(th, &pe)); - - CloseHandle(th); - - return res; -} - -static void startSteam(QWidget *widget) -{ - QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam", - QSettings::NativeFormat); - QString exe = steamSettings.value("SteamExe", "").toString(); - if (!exe.isEmpty()) { - exe = QString("\"%1\"").arg(exe); - // See if username and password supplied. If so, pass them into steam. - QStringList args; - QString username; - QString password; - if (Settings::instance().getSteamLogin(username, password)) { - args << "-login"; - args << username; - if (password != "") { - args << password; - } - } - if (!QProcess::startDetached(exe, args)) { - reportError(QObject::tr("Failed to start \"%1\"").arg(exe)); - } else { - QMessageBox::information( - widget, QObject::tr("Waiting"), - QObject::tr("Please press OK once you're logged into steam.")); - } - } -} - -template -QStringList toStringList(InputIterator current, InputIterator end) -{ - QStringList result; - for (; current != end; ++current) { - result.append(*current); - } - return result; -} - -bool checkService() -{ - SC_HANDLE serviceManagerHandle = NULL; - SC_HANDLE serviceHandle = NULL; - LPSERVICE_STATUS_PROCESS serviceStatus = NULL; - LPQUERY_SERVICE_CONFIG serviceConfig = NULL; - bool serviceRunning = true; - - DWORD bytesNeeded; - - try { - serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); - if (!serviceManagerHandle) { - qWarning("failed to open service manager (query status) (error %d)", GetLastError()); - throw 1; - } - - serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); - if (!serviceHandle) { - qWarning("failed to open EventLog service (query status) (error %d)", GetLastError()); - throw 2; - } - - if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded) - || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { - qWarning("failed to get size of service config (error %d)", GetLastError()); - throw 3; - } - - DWORD serviceConfigSize = bytesNeeded; - serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize); - if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) { - qWarning("failed to query service config (error %d)", GetLastError()); - throw 4; - } - - if (serviceConfig->dwStartType == SERVICE_DISABLED) { - qCritical("Windows Event Log service is disabled!"); - serviceRunning = false; - } - - if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded) - || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { - qWarning("failed to get size of service status (error %d)", GetLastError()); - throw 5; - } - - DWORD serviceStatusSize = bytesNeeded; - serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize); - if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) { - qWarning("failed to query service status (error %d)", GetLastError()); - throw 6; - } - - if (serviceStatus->dwCurrentState != SERVICE_RUNNING) { - qCritical("Windows Event Log service is not running"); - serviceRunning = false; - } - } - catch (int e) { - UNUSED_VAR(e); - serviceRunning = false; - } - - if (serviceStatus) { - LocalFree(serviceStatus); - } - if (serviceConfig) { - LocalFree(serviceConfig); - } - if (serviceHandle) { - CloseServiceHandle(serviceHandle); - } - if (serviceManagerHandle) { - CloseServiceHandle(serviceManagerHandle); - } - - return serviceRunning; -} - -OrganizerCore::OrganizerCore(const QSettings &initSettings) - : m_UserInterface(nullptr) - , m_PluginContainer(nullptr) - , m_GameName() - , m_CurrentProfile(nullptr) - , m_Settings(initSettings) - , m_Updater(NexusInterface::instance(m_PluginContainer)) - , m_AboutToRun() - , m_FinishedRun() - , m_ModInstalled() - , m_ModList(m_PluginContainer, this) - , m_PluginList(this) - , m_DirectoryRefresher() - , m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0)) - , m_DownloadManager(NexusInterface::instance(m_PluginContainer), this) - , m_InstallationManager() - , m_RefresherThread() - , m_AskForNexusPW(false) - , m_DirectoryUpdate(false) - , m_ArchivesInit(false) - , m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this)) -{ - m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); - m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers()); - - NexusInterface::instance(m_PluginContainer)->setCacheDirectory(m_Settings.getCacheDirectory()); - NexusInterface::instance(m_PluginContainer)->setNMMVersion(m_Settings.getNMMVersion()); - - MOBase::QuestionBoxMemory::init(initSettings.fileName()); - - m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); - m_InstallationManager.setDownloadDirectory(m_Settings.getDownloadDirectory()); - - connect(&m_DownloadManager, SIGNAL(downloadSpeed(QString, int)), this, - SLOT(downloadSpeed(QString, int))); - connect(&m_DirectoryRefresher, SIGNAL(refreshed()), this, - SLOT(directory_refreshed())); - - connect(&m_ModList, SIGNAL(removeOrigin(QString)), this, - SLOT(removeOrigin(QString))); - - connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(), - SIGNAL(loginSuccessful(bool)), this, SLOT(loginSuccessful(bool))); - connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(), - SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); - - // This seems awfully imperative - connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), - &m_Settings, SLOT(managedGameChanged(MOBase::IPluginGame const *))); - connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), - &m_DownloadManager, - SLOT(managedGameChanged(MOBase::IPluginGame const *))); - connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), - &m_PluginList, SLOT(managedGameChanged(MOBase::IPluginGame const *))); - - connect(&m_PluginList, &PluginList::writePluginsList, &m_PluginListsWriter, - &DelayedFileWriterBase::write); - - // make directory refresher run in a separate thread - m_RefresherThread.start(); - m_DirectoryRefresher.moveToThread(&m_RefresherThread); - - m_AskForNexusPW = initSettings.value("ask_for_nexuspw", true).toBool(); -} - -OrganizerCore::~OrganizerCore() -{ - m_RefresherThread.exit(); - m_RefresherThread.wait(); - - prepareStart(); - - // profile has to be cleaned up before the modinfo-buffer is cleared - delete m_CurrentProfile; - m_CurrentProfile = nullptr; - - ModInfo::clear(); - LogBuffer::cleanQuit(); - m_ModList.setProfile(nullptr); - // NexusInterface::instance()->cleanup(); - - delete m_DirectoryStructure; -} - -QString OrganizerCore::commitSettings(const QString &iniFile) -{ - if (!shellRename(iniFile + ".new", iniFile, true, qApp->activeWindow())) { - DWORD err = ::GetLastError(); - // make a second attempt using qt functions but if that fails print the - // error from the first attempt - if (!renameFile(iniFile + ".new", iniFile)) { - return windowsErrorString(err); - } - } - return QString(); -} - -QSettings::Status OrganizerCore::storeSettings(const QString &fileName) -{ - QSettings settings(fileName, QSettings::IniFormat); - if (m_UserInterface != nullptr) { - m_UserInterface->storeSettings(settings); - } - if (m_CurrentProfile != nullptr) { - settings.setValue("selected_profile", - m_CurrentProfile->name().toUtf8().constData()); - } - settings.setValue("ask_for_nexuspw", m_AskForNexusPW); - - settings.remove("customExecutables"); - settings.beginWriteArray("customExecutables"); - std::vector::const_iterator current, end; - m_ExecutablesList.getExecutables(current, end); - int count = 0; - for (; current != end; ++current) { - const Executable &item = *current; - settings.setArrayIndex(count++); - settings.setValue("title", item.m_Title); - settings.setValue("custom", item.isCustom()); - settings.setValue("toolbar", item.isShownOnToolbar()); - settings.setValue("ownicon", item.usesOwnIcon()); - if (item.isCustom()) { - settings.setValue("binary", item.m_BinaryInfo.absoluteFilePath()); - settings.setValue("arguments", item.m_Arguments); - settings.setValue("workingDirectory", item.m_WorkingDirectory); - settings.setValue("steamAppID", item.m_SteamAppID); - } - } - settings.endArray(); - - FileDialogMemory::save(settings); - - settings.sync(); - return settings.status(); -} - -void OrganizerCore::storeSettings() -{ - QString iniFile = qApp->property("dataPath").toString() + "/" - + QString::fromStdWString(AppConfig::iniFileName()); - if (QFileInfo(iniFile).exists()) { - if (!shellCopy(iniFile, iniFile + ".new", true, qApp->activeWindow())) { - QMessageBox::critical( - qApp->activeWindow(), tr("Failed to write settings"), - tr("An error occured trying to update MO settings to %1: %2") - .arg(iniFile, windowsErrorString(::GetLastError()))); - return; - } - } - - QString writeTarget = iniFile + ".new"; - - QSettings::Status result = storeSettings(writeTarget); - - if (result == QSettings::NoError) { - QString errMsg = commitSettings(iniFile); - if (!errMsg.isEmpty()) { - qWarning("settings file not writable, may be locked by another " - "application, trying direct write"); - writeTarget = iniFile; - result = storeSettings(iniFile); - } - } - if (result != QSettings::NoError) { - QString reason = result == QSettings::AccessError - ? tr("File is write protected") - : result == QSettings::FormatError - ? tr("Invalid file format (probably a bug)") - : tr("Unknown error %1").arg(result); - QMessageBox::critical( - qApp->activeWindow(), tr("Failed to write settings"), - tr("An error occured trying to write back MO settings to %1: %2") - .arg(writeTarget, reason)); - } -} - -bool OrganizerCore::testForSteam() -{ - size_t currentSize = 1024; - std::unique_ptr processIDs; - DWORD bytesReturned; - bool success = false; - while (!success) { - processIDs.reset(new DWORD[currentSize]); - if (!::EnumProcesses(processIDs.get(), - static_cast(currentSize) * sizeof(DWORD), - &bytesReturned)) { - qWarning("failed to determine if steam is running"); - return true; - } - if (bytesReturned == (currentSize * sizeof(DWORD))) { - // maximum size used, list probably truncated - currentSize *= 2; - } else { - success = true; - } - } - TCHAR processName[MAX_PATH]; - for (unsigned int i = 0; i < bytesReturned / sizeof(DWORD); ++i) { - memset(processName, '\0', sizeof(TCHAR) * MAX_PATH); - if (processIDs[i] != 0) { - HANDLE process = ::OpenProcess( - PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processIDs[i]); - - if (process != nullptr) { - - ON_BLOCK_EXIT([&]() { - if (process != INVALID_HANDLE_VALUE) - ::CloseHandle(process); - }); - - HMODULE module; - DWORD ignore; - - // first module in a process is always the binary - if (EnumProcessModules(process, &module, sizeof(HMODULE) * 1, - &ignore)) { - ::GetModuleBaseName(process, module, processName, MAX_PATH); - if ((_tcsicmp(processName, TEXT("steam.exe")) == 0) - || (_tcsicmp(processName, TEXT("steamservice.exe")) == 0)) { - return true; - } - } - } - } - } - - return false; -} - -void OrganizerCore::updateExecutablesList(QSettings &settings) -{ - if (m_PluginContainer == nullptr) { - qCritical("can't update executables list now"); - return; - } - - m_ExecutablesList.init(managedGame()); - - qDebug("setting up configured executables"); - - int numCustomExecutables = settings.beginReadArray("customExecutables"); - for (int i = 0; i < numCustomExecutables; ++i) { - settings.setArrayIndex(i); - - Executable::Flags flags; - if (settings.value("custom", true).toBool()) - flags |= Executable::CustomExecutable; - if (settings.value("toolbar", false).toBool()) - flags |= Executable::ShowInToolbar; - if (settings.value("ownicon", false).toBool()) - flags |= Executable::UseApplicationIcon; - - m_ExecutablesList.addExecutable( - settings.value("title").toString(), settings.value("binary").toString(), - settings.value("arguments").toString(), - settings.value("workingDirectory", "").toString(), - settings.value("steamAppID", "").toString(), flags); - } - - settings.endArray(); - - // TODO this has nothing to do with executables list move to an appropriate - // function! - ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, - m_PluginContainer, m_Settings.displayForeign(), managedGame()); -} - -void OrganizerCore::setUserInterface(IUserInterface *userInterface, - QWidget *widget) -{ - storeSettings(); - - m_UserInterface = userInterface; - - if (widget != nullptr) { - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), widget, - SLOT(modlistChanged(QModelIndex, int))); - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), widget, - SLOT(modlistChanged(QModelIndexList, int))); - connect(&m_ModList, SIGNAL(showMessage(QString)), widget, - SLOT(showMessage(QString))); - connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), widget, - SLOT(modRenamed(QString, QString))); - connect(&m_ModList, SIGNAL(modUninstalled(QString)), widget, - SLOT(modRemoved(QString))); - connect(&m_ModList, SIGNAL(removeSelectedMods()), widget, - SLOT(removeMod_clicked())); - connect(&m_ModList, SIGNAL(clearOverwrite()), widget, - SLOT(clearOverwrite())); - connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), widget, - SLOT(displayColumnSelection(QPoint))); - connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), widget, - SLOT(fileMoved(QString, QString, QString))); - connect(&m_ModList, SIGNAL(modorder_changed()), widget, - SLOT(modorder_changed())); - connect(&m_PluginList, SIGNAL(writePluginsList()), widget, - SLOT(esplist_changed())); - connect(&m_PluginList, SIGNAL(esplist_changed()), widget, - SLOT(esplist_changed())); - connect(&m_DownloadManager, SIGNAL(showMessage(QString)), widget, - SLOT(showMessage(QString))); - } - - m_InstallationManager.setParentWidget(widget); - m_Updater.setUserInterface(widget); - - if (userInterface != nullptr) { - // this currently wouldn't work reliably if the ui isn't initialized yet to - // display the result - if (isOnline() && !m_Settings.offlineMode()) { - m_Updater.testForUpdate(); - } else { - qDebug("user doesn't seem to be connected to the internet"); - } - } -} - -void OrganizerCore::connectPlugins(PluginContainer *container) -{ - m_DownloadManager.setSupportedExtensions( - m_InstallationManager.getSupportedExtensions()); - m_PluginContainer = container; - m_Updater.setPluginContainer(m_PluginContainer); - m_DownloadManager.setPluginContainer(m_PluginContainer); - m_ModList.setPluginContainer(m_PluginContainer); - - if (!m_GameName.isEmpty()) { - m_GamePlugin = m_PluginContainer->managedGame(m_GameName); - emit managedGameChanged(m_GamePlugin); - } -} - -void OrganizerCore::disconnectPlugins() -{ - m_AboutToRun.disconnect_all_slots(); - m_FinishedRun.disconnect_all_slots(); - m_ModInstalled.disconnect_all_slots(); - m_ModList.disconnectSlots(); - m_PluginList.disconnectSlots(); - m_Updater.setPluginContainer(nullptr); - m_DownloadManager.setPluginContainer(nullptr); - m_ModList.setPluginContainer(nullptr); - - m_Settings.clearPlugins(); - m_GamePlugin = nullptr; - m_PluginContainer = nullptr; -} - -void OrganizerCore::setManagedGame(MOBase::IPluginGame *game) -{ - m_GameName = game->gameName(); - m_GamePlugin = game; - qApp->setProperty("managed_game", QVariant::fromValue(m_GamePlugin)); - emit managedGameChanged(m_GamePlugin); -} - -Settings &OrganizerCore::settings() -{ - return m_Settings; -} - -bool OrganizerCore::nexusLogin(bool retry) -{ - NXMAccessManager *accessManager - = NexusInterface::instance(m_PluginContainer)->getAccessManager(); - - if ((accessManager->loginAttempted() || accessManager->loggedIn()) - && !retry) { - // previous attempt, maybe even successful - return false; - } else { - QString username, password; - if ((!retry && m_Settings.getNexusLogin(username, password)) - || (m_AskForNexusPW && queryLogin(username, password))) { - // credentials stored or user entered them manually - qDebug("attempt login with username %s", qUtf8Printable(username)); - accessManager->login(username, password); - return true; - } else { - // no credentials stored and user didn't enter them - accessManager->refuseLogin(); - return false; - } - } -} - -bool OrganizerCore::queryLogin(QString &username, QString &password) -{ - CredentialsDialog dialog(qApp->activeWindow()); - int res = dialog.exec(); - if (dialog.neverAsk()) { - m_AskForNexusPW = false; - } - if (res == QDialog::Accepted) { - username = dialog.username(); - password = dialog.password(); - if (dialog.store()) { - m_Settings.setNexusLogin(username, password); - } - return true; - } else { - return false; - } -} - -void OrganizerCore::startMOUpdate() -{ - if (nexusLogin()) { - m_PostLoginTasks.append([&]() { m_Updater.startUpdate(); }); - } else { - m_Updater.startUpdate(); - } -} - -void OrganizerCore::downloadRequestedNXM(const QString &url) -{ - qDebug("download requested: %s", qUtf8Printable(url)); - if (nexusLogin()) { - m_PendingDownloads.append(url); - } else { - m_DownloadManager.addNXMDownload(url); - } -} - -void OrganizerCore::externalMessage(const QString &message) -{ - if (MOShortcut moshortcut{ message } ) { - if(moshortcut.hasExecutable()) - runShortcut(moshortcut); - } - else if (isNxmLink(message)) { - MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); - downloadRequestedNXM(message); - } -} - -void OrganizerCore::downloadRequested(QNetworkReply *reply, QString gameName, int modID, - const QString &fileName) -{ - try { - if (m_DownloadManager.addDownload(reply, QStringList(), fileName, gameName, modID, 0, - new ModRepositoryFileInfo(gameName, modID))) { - MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); - } - } catch (const std::exception &e) { - MessageDialog::showMessage(tr("Download failed"), qApp->activeWindow()); - qCritical("exception starting download: %s", e.what()); - } -} - -void OrganizerCore::removeOrigin(const QString &name) -{ - FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(name)); - origin.enable(false); - refreshLists(); -} - -void OrganizerCore::downloadSpeed(const QString &serverName, int bytesPerSecond) -{ - m_Settings.setDownloadSpeed(serverName, bytesPerSecond); -} - -InstallationManager *OrganizerCore::installationManager() -{ - return &m_InstallationManager; -} - -bool OrganizerCore::createDirectory(const QString &path) { - if (!QDir(path).exists() && !QDir().mkpath(path)) { - QMessageBox::critical(nullptr, QObject::tr("Error"), - QObject::tr("Failed to create \"%1\". Your user " - "account probably lacks permission.") - .arg(QDir::toNativeSeparators(path))); - return false; - } else { - return true; - } -} - -bool OrganizerCore::bootstrap() { - return createDirectory(m_Settings.getProfileDirectory()) && - createDirectory(m_Settings.getModDirectory()) && - createDirectory(m_Settings.getDownloadDirectory()) && - createDirectory(m_Settings.getOverwriteDirectory()) && - createDirectory(QString::fromStdWString(crashDumpsPath())) && cycleDiagnostics(); -} - -void OrganizerCore::createDefaultProfile() -{ - QString profilesPath = settings().getProfileDirectory(); - if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size() - == 0) { - Profile newProf("Default", managedGame(), false); - } -} - -void OrganizerCore::prepareVFS() -{ - m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString())); -} - -void OrganizerCore::updateVFSParams(int logLevel, int crashDumpsType, QString executableBlacklist) { - setGlobalCrashDumpsType(crashDumpsType); - m_USVFS.updateParams(logLevel, crashDumpsType, executableBlacklist); -} - -bool OrganizerCore::cycleDiagnostics() { - if (int maxDumps = settings().crashDumpsMax()) - removeOldFiles(QString::fromStdWString(crashDumpsPath()), "*.dmp", maxDumps, QDir::Time|QDir::Reversed); - return true; -} - -//static -void OrganizerCore::setGlobalCrashDumpsType(int crashDumpsType) { - m_globalCrashDumpsType = ::crashDumpsType(crashDumpsType); -} - -//static -std::wstring OrganizerCore::crashDumpsPath() { - return ( - qApp->property("dataPath").toString() + "/" - + QString::fromStdWString(AppConfig::dumpsDir()) - ).toStdWString(); -} - -bool OrganizerCore::getArchiveParsing() const -{ - return m_ArchiveParsing; -} - -void OrganizerCore::setArchiveParsing(const bool archiveParsing) -{ - m_ArchiveParsing = archiveParsing; -} - -void OrganizerCore::setCurrentProfile(const QString &profileName) -{ - if ((m_CurrentProfile != nullptr) - && (profileName == m_CurrentProfile->name())) { - return; - } - - QDir profileBaseDir(settings().getProfileDirectory()); - QString profileDir = profileBaseDir.absoluteFilePath(profileName); - - if (!QDir(profileDir).exists()) { - // selected profile doesn't exist. Ensure there is at least one profile, - // then pick any one - createDefaultProfile(); - - profileDir = profileBaseDir.absoluteFilePath( - profileBaseDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot).at(0)); - } - - Profile *newProfile = new Profile(QDir(profileDir), managedGame()); - - delete m_CurrentProfile; - m_CurrentProfile = newProfile; - m_ModList.setProfile(newProfile); - - if (m_CurrentProfile->invalidationActive(nullptr)) { - m_CurrentProfile->activateInvalidation(); - } else { - m_CurrentProfile->deactivateInvalidation(); - } - - connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this, SLOT(modStatusChanged(uint))); - connect(m_CurrentProfile, SIGNAL(modStatusChanged(QList)), this, SLOT(modStatusChanged(QList))); - refreshDirectoryStructure(); - - //This line is not actually needed and was only added to allow some - //outside detection of Mo2 profile change. (like BaobobMiller utility) - if (m_CurrentProfile != nullptr) { - settings().directInterface().setValue("selected_profile", - m_CurrentProfile->name().toUtf8().constData()); - } -} - -MOBase::IModRepositoryBridge *OrganizerCore::createNexusBridge() const -{ - return new NexusBridge(m_PluginContainer); -} - -QString OrganizerCore::profileName() const -{ - if (m_CurrentProfile != nullptr) { - return m_CurrentProfile->name(); - } else { - return ""; - } -} - -QString OrganizerCore::profilePath() const -{ - if (m_CurrentProfile != nullptr) { - return m_CurrentProfile->absolutePath(); - } else { - return ""; - } -} - -QString OrganizerCore::downloadsPath() const -{ - return QDir::fromNativeSeparators(m_Settings.getDownloadDirectory()); -} - -QString OrganizerCore::overwritePath() const -{ - return QDir::fromNativeSeparators(m_Settings.getOverwriteDirectory()); -} - -QString OrganizerCore::basePath() const -{ - return QDir::fromNativeSeparators(m_Settings.getBaseDirectory()); -} - -MOBase::VersionInfo OrganizerCore::appVersion() const -{ - return m_Updater.getVersion(); -} - -MOBase::IModInterface *OrganizerCore::getMod(const QString &name) const -{ - unsigned int index = ModInfo::getIndex(name); - return index == UINT_MAX ? nullptr : ModInfo::getByIndex(index).data(); -} - -MOBase::IPluginGame *OrganizerCore::getGame(const QString &name) const -{ - for (IPluginGame *game : m_PluginContainer->plugins()) { - if (game->gameShortName().compare(name, Qt::CaseInsensitive) == 0) - return game; - } - return nullptr; -} - -MOBase::IModInterface *OrganizerCore::createMod(GuessedValue &name) -{ - bool merge = false; - if (!m_InstallationManager.testOverwrite(name, &merge)) { - return nullptr; - } - - m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); - - QString targetDirectory - = QDir::fromNativeSeparators(m_Settings.getModDirectory()) - .append("/") - .append(name); - - QSettings settingsFile(targetDirectory + "/meta.ini", QSettings::IniFormat); - - if (!merge) { - settingsFile.setValue("modid", 0); - settingsFile.setValue("version", ""); - settingsFile.setValue("newestVersion", ""); - settingsFile.setValue("category", 0); - settingsFile.setValue("installationFile", ""); - - settingsFile.remove("installedFiles"); - settingsFile.beginWriteArray("installedFiles", 0); - settingsFile.endArray(); - } - - return ModInfo::createFrom(m_PluginContainer, m_GamePlugin, QDir(targetDirectory), &m_DirectoryStructure) - .data(); -} - -bool OrganizerCore::removeMod(MOBase::IModInterface *mod) -{ - unsigned int index = ModInfo::getIndex(mod->name()); - if (index == UINT_MAX) { - return mod->remove(); - } else { - return ModInfo::removeMod(index); - } -} - -void OrganizerCore::modDataChanged(MOBase::IModInterface *) -{ - refreshModList(false); -} - -QVariant OrganizerCore::pluginSetting(const QString &pluginName, - const QString &key) const -{ - return m_Settings.pluginSetting(pluginName, key); -} - -void OrganizerCore::setPluginSetting(const QString &pluginName, - const QString &key, const QVariant &value) -{ - m_Settings.setPluginSetting(pluginName, key, value); -} - -QVariant OrganizerCore::persistent(const QString &pluginName, - const QString &key, - const QVariant &def) const -{ - return m_Settings.pluginPersistent(pluginName, key, def); -} - -void OrganizerCore::setPersistent(const QString &pluginName, const QString &key, - const QVariant &value, bool sync) -{ - m_Settings.setPluginPersistent(pluginName, key, value, sync); -} - -QString OrganizerCore::pluginDataPath() const -{ - return qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()) - + "/data"; -} - -MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName, - const QString &initModName) -{ - if (m_CurrentProfile == nullptr) { - return nullptr; - } - - if (m_InstallationManager.isRunning()) { - QMessageBox::information( - qApp->activeWindow(), tr("Installation cancelled"), - tr("Another installation is currently in progress."), QMessageBox::Ok); - return nullptr; - } - - bool hasIniTweaks = false; - GuessedValue modName; - if (!initModName.isEmpty()) { - modName.update(initModName, GUESS_USER); - } - m_CurrentProfile->writeModlistNow(); - m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); - if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { - MessageDialog::showMessage(tr("Installation successful"), - qApp->activeWindow()); - refreshModList(); - - int modIndex = ModInfo::getIndex(modName); - if (modIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if (hasIniTweaks && (m_UserInterface != nullptr) - && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), - tr("This mod contains ini tweaks. Do you " - "want to configure them now?"), - QMessageBox::Yes | QMessageBox::No) - == QMessageBox::Yes)) { - m_UserInterface->displayModInformation(modInfo, modIndex, - ModInfoDialog::TAB_INIFILES); - } - m_ModInstalled(modName); - m_DownloadManager.markInstalled(fileName); - emit modInstalled(modName); - return modInfo.data(); - } else { - reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); - } - } else if (m_InstallationManager.wasCancelled()) { - QMessageBox::information(qApp->activeWindow(), tr("Installation cancelled"), - tr("The mod was not installed completely."), - QMessageBox::Ok); - } - return nullptr; -} - -void OrganizerCore::installDownload(int index) -{ - if (m_InstallationManager.isRunning()) { - QMessageBox::information( - qApp->activeWindow(), tr("Installation cancelled"), - tr("Another installation is currently in progress."), QMessageBox::Ok); - return; - } - - try { - QString fileName = m_DownloadManager.getFilePath(index); - QString gameName = m_DownloadManager.getGameName(index); - int modID = m_DownloadManager.getModID(index); - int fileID = m_DownloadManager.getFileInfo(index)->fileID; - GuessedValue modName; - - // see if there already are mods with the specified mod id - if (modID != 0) { - std::vector modInfo = ModInfo::getByModID(gameName, modID); - for (auto iter = modInfo.begin(); iter != modInfo.end(); ++iter) { - std::vector flags = (*iter)->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) - == flags.end()) { - modName.update((*iter)->name(), GUESS_PRESET); - (*iter)->saveMeta(); - } - } - } - - m_CurrentProfile->writeModlistNow(); - - bool hasIniTweaks = false; - m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); - if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { - MessageDialog::showMessage(tr("Installation successful"), - qApp->activeWindow()); - refreshModList(); - - int modIndex = ModInfo::getIndex(modName); - if (modIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - modInfo->addInstalledFile(modID, fileID); - - if (hasIniTweaks && m_UserInterface != nullptr - && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), - tr("This mod contains ini tweaks. Do you " - "want to configure them now?"), - QMessageBox::Yes | QMessageBox::No) - == QMessageBox::Yes)) { - m_UserInterface->displayModInformation(modInfo, modIndex, - ModInfoDialog::TAB_INIFILES); - } - - m_ModInstalled(modName); - } else { - reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); - } - m_DownloadManager.markInstalled(index); - - emit modInstalled(modName); - } else if (m_InstallationManager.wasCancelled()) { - QMessageBox::information( - qApp->activeWindow(), tr("Installation cancelled"), - tr("The mod was not installed completely."), QMessageBox::Ok); - } - } catch (const std::exception &e) { - reportError(e.what()); - } -} - -QString OrganizerCore::resolvePath(const QString &fileName) const -{ - if (m_DirectoryStructure == nullptr) { - return QString(); - } - const FileEntry::Ptr file - = m_DirectoryStructure->searchFile(ToWString(fileName), nullptr); - if (file.get() != nullptr) { - return ToQString(file->getFullPath()); - } else { - return QString(); - } -} - -QStringList OrganizerCore::listDirectories(const QString &directoryName) const -{ - QStringList result; - DirectoryEntry *dir = m_DirectoryStructure; - if (!directoryName.isEmpty()) - dir = dir->findSubDirectoryRecursive(ToWString(directoryName)); - if (dir != nullptr) { - std::vector::iterator current, end; - dir->getSubDirectories(current, end); - for (; current != end; ++current) { - result.append(ToQString((*current)->getName())); - } - } - return result; -} - -QStringList OrganizerCore::findFiles( - const QString &path, - const std::function &filter) const -{ - QStringList result; - DirectoryEntry *dir = m_DirectoryStructure; - if (!path.isEmpty()) - dir = dir->findSubDirectoryRecursive(ToWString(path)); - if (dir != nullptr) { - std::vector files = dir->getFiles(); - foreach (FileEntry::Ptr file, files) { - if (filter(ToQString(file->getFullPath()))) { - result.append(ToQString(file->getFullPath())); - } - } - } else { - qWarning("directory not found: %1", qUtf8Printable(path)); - } - return result; -} - -QStringList OrganizerCore::getFileOrigins(const QString &fileName) const -{ - QStringList result; - const FileEntry::Ptr file = m_DirectoryStructure->searchFile( - ToWString(QFileInfo(fileName).fileName()), nullptr); - - if (file.get() != nullptr) { - result.append(ToQString( - m_DirectoryStructure->getOriginByID(file->getOrigin()).getName())); - foreach (auto i, file->getAlternatives()) { - result.append( - ToQString(m_DirectoryStructure->getOriginByID(i.first).getName())); - } - } else { - qWarning("file not found: %1", qUtf8Printable(fileName)); - } - return result; -} - -QList OrganizerCore::findFileInfos( - const QString &path, - const std::function &filter) - const -{ - QList result; - DirectoryEntry *dir = m_DirectoryStructure; - if (!path.isEmpty()) - dir = dir->findSubDirectoryRecursive(ToWString(path)); - if (dir != nullptr) { - std::vector files = dir->getFiles(); - foreach (FileEntry::Ptr file, files) { - IOrganizer::FileInfo info; - info.filePath = ToQString(file->getFullPath()); - bool fromArchive = false; - info.origins.append(ToQString( - m_DirectoryStructure->getOriginByID(file->getOrigin(fromArchive)) - .getName())); - info.archive = fromArchive ? ToQString(file->getArchive().first) : ""; - foreach (auto idx, file->getAlternatives()) { - info.origins.append( - ToQString(m_DirectoryStructure->getOriginByID(idx.first).getName())); - } - - if (filter(info)) { - result.append(info); - } - } - } - return result; -} - -DownloadManager *OrganizerCore::downloadManager() -{ - return &m_DownloadManager; -} - -PluginList *OrganizerCore::pluginList() -{ - return &m_PluginList; -} - -ModList *OrganizerCore::modList() -{ - return &m_ModList; -} - -QStringList OrganizerCore::modsSortedByProfilePriority() const -{ - QStringList res; - for (int i = currentProfile()->getPriorityMinimum(); - i < currentProfile()->getPriorityMinimum() + (int)currentProfile()->numRegularMods(); - ++i) { - int modIndex = currentProfile()->modIndexByPriority(i); - auto modInfo = ModInfo::getByIndex(modIndex); - if (!modInfo->hasFlag(ModInfo::FLAG_OVERWRITE) && - !modInfo->hasFlag(ModInfo::FLAG_BACKUP)) { - res.push_back(ModInfo::getByIndex(modIndex)->name()); - } - } - return res; -} - -void OrganizerCore::spawnBinary(const QFileInfo &binary, - const QString &arguments, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries) -{ - DWORD processExitCode = 0; - HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, customOverwrite, forcedLibraries, &processExitCode); - if (processHandle != INVALID_HANDLE_VALUE) { - refreshDirectoryStructure(); - // need to remove our stored load order because it may be outdated if a foreign tool changed the - // file time. After removing that file, refreshESPList will use the file time as the order - if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) { - qDebug("removing loadorder.txt"); - QFile::remove(m_CurrentProfile->getLoadOrderFileName()); - } - refreshDirectoryStructure(); - - refreshESPList(true); - savePluginList(); - - //These callbacks should not fiddle with directoy structure and ESPs. - m_FinishedRun(binary.absoluteFilePath(), processExitCode); - } -} - -HANDLE OrganizerCore::spawnBinaryDirect(const QFileInfo &binary, - const QString &arguments, - const QString &profileName, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - LPDWORD exitCode) -{ - HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); - if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) { - std::unique_ptr dlg; - ILockedWaitingForProcess* uilock = nullptr; - - if (m_UserInterface != nullptr) { - uilock = m_UserInterface->lock(); - } - else { - // i.e. when running command line shortcuts there is no m_UserInterface - dlg.reset(new LockedDialog); - dlg->show(); - dlg->setEnabled(true); - uilock = dlg.get(); - } - - ON_BLOCK_EXIT([&]() { - if (m_UserInterface != nullptr) { - m_UserInterface->unlock(); - } }); - - DWORD ignoreExitCode; - waitForProcessCompletion(processHandle, exitCode ? exitCode : &ignoreExitCode, uilock); - cycleDiagnostics(); - } - - return processHandle; -} - - -HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, - const QString &arguments, - const QString &profileName, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries) -{ - prepareStart(); - - if (!binary.exists()) { - reportError( - tr("Executable not found: %1").arg(qUtf8Printable(binary.absoluteFilePath()))); - return INVALID_HANDLE_VALUE; - } - - if (!steamAppID.isEmpty()) { - ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str()); - } else { - ::SetEnvironmentVariableW(L"SteamAPPId", - ToWString(m_Settings.getSteamAppID()).c_str()); - } - - QWidget *window = qApp->activeWindow(); - if ((window != nullptr) && (!window->isVisible())) { - window = nullptr; - } - - // This could possibly be extracted somewhere else but it's probably for when - // we have more than one provider of game registration. - if ((QFileInfo( - managedGame()->gameDirectory().absoluteFilePath("steam_api.dll")) - .exists() - || QFileInfo(managedGame()->gameDirectory().absoluteFilePath( - "steam_api64.dll")) - .exists()) - && (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) { - if (!testForSteam()) { - QDialogButtonBox::StandardButton result; - result = QuestionBoxMemory::query(window, "steamQuery", binary.fileName(), - tr("Start Steam?"), - tr("Steam is required to be running already to correctly start the game. " - "Should MO try to start steam now?"), - QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); - if (result == QDialogButtonBox::Yes) { - startSteam(window); - } else if(result == QDialogButtonBox::Cancel) { - return INVALID_HANDLE_VALUE; - } - } - } - - while (m_DirectoryUpdate) { - ::Sleep(100); - QCoreApplication::processEvents(); - } - - // need to make sure all data is saved before we start the application - if (m_CurrentProfile != nullptr) { - m_CurrentProfile->writeModlistNow(true); - } - - // TODO: should also pass arguments - if (m_AboutToRun(binary.absoluteFilePath())) { - try { - m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); - m_USVFS.updateForcedLibraries(forcedLibraries); - - } catch (const UsvfsConnectorException &e) { - qDebug(e.what()); - return INVALID_HANDLE_VALUE; - } catch (const std::exception &e) { - QMessageBox::warning(window, tr("Error"), e.what()); - return INVALID_HANDLE_VALUE; - } - - // Check if the Windows Event Logging service is running. For some reason, this seems to be - // critical to the successful running of usvfs. - if (!checkService()) { - if (QuestionBoxMemory::query(window, QString("eventLogService"), binary.fileName(), - tr("Windows Event Log Error"), - tr("The Windows Event Log service is disabled and/or not running. This prevents" - " USVFS from running properly. Your mods may not be working in the executable" - " that you are launching. Note that you may have to restart MO and/or your PC" - " after the service is fixed.\n\nContinue launching %1?").arg(binary.fileName()), - QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) { - return INVALID_HANDLE_VALUE; - } - } - - for (auto exec : settings().executablesBlacklist().split(";")) { - if (exec.compare(binary.fileName(), Qt::CaseInsensitive) == 0) { - if (QuestionBoxMemory::query(window, QString("blacklistedExecutable"), binary.fileName(), - tr("Blacklisted Executable"), - tr("The executable you are attempted to launch is blacklisted in the virtual file" - " system. This will likely prevent the executable, and any executables that are" - " launched by this one, from seeing any mods. This could extend to INI files, save" - " games and any other virtualized files.\n\nContinue launching %1?").arg(binary.fileName()), - QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) { - return INVALID_HANDLE_VALUE; - } - } - } - - QString modsPath = settings().getModDirectory(); - - // Check if this a request with either an executable or a working directory under our mods folder - // then will start the process in a virtualized "environment" with the appropriate paths fixed: - // (i.e. mods\FNIS\path\exe => game\data\path\exe) - QString cwdPath = currentDirectory.absolutePath(); - bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); - QString binPath = binary.absoluteFilePath(); - bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); - if (virtualizedCwd || virtualizedBin) { - if (virtualizedCwd) { - int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); - QString adjustedCwd = cwdPath.mid(cwdOffset, -1); - cwdPath = m_GamePlugin->dataDirectory().absolutePath(); - if (cwdOffset >= 0) - cwdPath += adjustedCwd; - - } - - if (virtualizedBin) { - int binOffset = binPath.indexOf('/', modsPath.length() + 1); - QString adjustedBin = binPath.mid(binOffset, -1); - binPath = m_GamePlugin->dataDirectory().absolutePath(); - if (binOffset >= 0) - binPath += adjustedBin; - } - - QString cmdline - = QString("launch \"%1\" \"%2\" %3") - .arg(QDir::toNativeSeparators(cwdPath), - QDir::toNativeSeparators(binPath), arguments); - - qDebug() << "Spawning proxyed process <" << cmdline << ">"; - - return startBinary(QFileInfo(QCoreApplication::applicationFilePath()), - cmdline, QCoreApplication::applicationDirPath(), true); - } else { - qDebug() << "Spawning direct process <" << binPath << "," << arguments << "," << cwdPath << ">"; - return startBinary(binary, arguments, currentDirectory, true); - } - } else { - qDebug("start of \"%s\" canceled by plugin", - qUtf8Printable(binary.absoluteFilePath())); - return INVALID_HANDLE_VALUE; - } -} - -HANDLE OrganizerCore::runShortcut(const MOShortcut& shortcut) -{ - if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance()) - throw std::runtime_error( - QString("Refusing to run executable from different instance %1:%2") - .arg(shortcut.instance(),shortcut.executable()) - .toLocal8Bit().constData()); - - Executable& exe = m_ExecutablesList.find(shortcut.executable()); - auto forcedLibaries = m_CurrentProfile->determineForcedLibraries(shortcut.executable()); - if (!m_CurrentProfile->forcedLibrariesEnabled(shortcut.executable())) { - forcedLibaries.clear(); - } - - return spawnBinaryDirect( - exe.m_BinaryInfo, exe.m_Arguments, - m_CurrentProfile->name(), - exe.m_WorkingDirectory.length() != 0 - ? exe.m_WorkingDirectory - : exe.m_BinaryInfo.absolutePath(), - exe.m_SteamAppID, - "", - forcedLibaries); -} - -HANDLE OrganizerCore::startApplication(const QString &executable, - const QStringList &args, - const QString &cwd, - const QString &profile) -{ - QFileInfo binary; - QString arguments = args.join(" "); - QString currentDirectory = cwd; - QString profileName = profile; - if (profile.length() == 0) { - if (m_CurrentProfile != nullptr) { - profileName = m_CurrentProfile->name(); - } else { - throw MyException(tr("No profile set")); - } - } - QString steamAppID; - QString customOverwrite; - QList forcedLibraries; - if (executable.contains('\\') || executable.contains('/')) { - // file path - - binary = QFileInfo(executable); - if (binary.isRelative()) { - // relative path, should be relative to game directory - binary = QFileInfo( - managedGame()->gameDirectory().absoluteFilePath(executable)); - } - if (cwd.length() == 0) { - currentDirectory = binary.absolutePath(); - } - try { - const Executable &exe = m_ExecutablesList.findByBinary(binary); - steamAppID = exe.m_SteamAppID; - customOverwrite - = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) - .toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); - } - } catch (const std::runtime_error &) { - // nop - } - } else { - // only a file name, search executables list - try { - const Executable &exe = m_ExecutablesList.find(executable); - steamAppID = exe.m_SteamAppID; - customOverwrite - = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) - .toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); - } - if (arguments == "") { - arguments = exe.m_Arguments; - } - binary = exe.m_BinaryInfo; - if (cwd.length() == 0) { - currentDirectory = exe.m_WorkingDirectory; - } - } catch (const std::runtime_error &) { - qWarning("\"%s\" not set up as executable", - qUtf8Printable(executable)); - binary = QFileInfo(executable); - } - } - - return spawnBinaryDirect(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); -} - -bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode) -{ - if (!Settings::instance().lockGUI()) - return true; - - ILockedWaitingForProcess* uilock = nullptr; - if (m_UserInterface != nullptr) { - uilock = m_UserInterface->lock(); - } - - ON_BLOCK_EXIT([&] () { - if (m_UserInterface != nullptr) { - m_UserInterface->unlock(); - } }); - return waitForProcessCompletion(handle, exitCode, uilock); -} - -bool OrganizerCore::waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) -{ - bool originalHandle = true; - bool newHandle = true; - bool uiunlocked = false; - - DWORD currentPID = 0; - QString processName; - auto waitForChildUntil = GetTickCount64(); - if (handle != INVALID_HANDLE_VALUE) { - currentPID = GetProcessId(handle); - processName = QString::fromStdWString(getProcessName(handle)); - } - - // Certain process names we wish to "hide" for aesthetic reason: - bool waitingOnHidden = false; - std::vector hiddenList; - hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); - for (QString hide : hiddenList) - if (processName.contains(hide, Qt::CaseInsensitive)) - waitingOnHidden = true; - // The main reason for adding the hidden list is to hide the MO proxy we use to spawn virtualized processes. - // On the one hand we want to display the real executable without it feeling laggy, on the other we don't want - // to requery processes all the time if for some reason we are waiting on hidden processes and find no "unhidden" - // process. For this reason we use exponential backoff and also start with a delibrately low value to improve - // the responsiveness of the initial update - DWORD64 nextHiddenCheck = GetTickCount64(); - DWORD64 nextHiddenCheckDelay = 50; - - constexpr DWORD INPUT_EVENT = WAIT_OBJECT_0 + 1; - DWORD res = WAIT_TIMEOUT; - while (handle != INVALID_HANDLE_VALUE && (newHandle || res == WAIT_TIMEOUT || res == INPUT_EVENT)) - { - if (newHandle) { - processName += QString(" (%1)").arg(currentPID); - if (uilock) - uilock->setProcessName(processName); - qDebug() << "Waiting for" - << (originalHandle ? "spawned" : "usvfs") - << "process completion :" << qUtf8Printable(processName); - newHandle = false; - } - - // Wait for a an event on the handle, a key press, mouse click or timeout - res = MsgWaitForMultipleObjects(1, &handle, FALSE, 200, QS_KEY | QS_MOUSEBUTTON); - if (res == WAIT_FAILED) { - qWarning() << "Failed waiting for process completion : MsgWaitForMultipleObjects WAIT_FAILED" << GetLastError(); - break; - } - - // keep processing events so the app doesn't appear dead - QCoreApplication::sendPostedEvents(); - QCoreApplication::processEvents(); - - if (uilock && uilock->unlockForced()) { - uiunlocked = true; - break; - } - - if (res == WAIT_OBJECT_0) { - // process we were waiting on has completed - if (originalHandle && exitCode && !::GetExitCodeProcess(handle, exitCode)) - qWarning() << "Failed getting exit code of complete process :" << GetLastError(); - CloseHandle(handle); - handle = INVALID_HANDLE_VALUE; - originalHandle = false; - // if the previous process spawned a child process and immediately exits we may miss it if we check immediately - waitForChildUntil = GetTickCount64() + 800; - } - - // search for another process to wait on if either: - // 1. we just completed waiting for a process and need to find/wait for an inject child - // 2. we are currently waiting on a hidden process so periodically check if there is a non-hidden process to wait on - bool firstIteration = true; - while ((handle == INVALID_HANDLE_VALUE && GetTickCount64() <= waitForChildUntil) - || (waitingOnHidden && GetTickCount64() >= nextHiddenCheck)) - { - if (firstIteration) - firstIteration = false; - else { - QThread::msleep(200); - QCoreApplication::sendPostedEvents(); - QCoreApplication::processEvents(); - } - - // search if there is another usvfs process active - handle = findAndOpenAUSVFSProcess(hiddenList, currentPID); - waitingOnHidden = false; - newHandle = handle != INVALID_HANDLE_VALUE; - if (newHandle) { - currentPID = GetProcessId(handle); - processName = QString::fromStdWString(getProcessName(handle)); - for (QString hide : hiddenList) - if (processName.contains(hide, Qt::CaseInsensitive)) - waitingOnHidden = true; - } - if (waitingOnHidden) { - nextHiddenCheck = GetTickCount64() + nextHiddenCheckDelay; - nextHiddenCheckDelay = std::min(nextHiddenCheckDelay * 2, (DWORD64) 2000); - } - else { - nextHiddenCheck = GetTickCount64(); - nextHiddenCheckDelay = 200; - } - } - } - - if (res == WAIT_OBJECT_0) - qDebug() << "Waiting for process completion successfull"; - else if (uiunlocked) - qDebug() << "Waiting for process completion aborted by UI"; - else - qDebug() << "Waiting for process completion not successfull :" << res; - - if (handle != INVALID_HANDLE_VALUE) - ::CloseHandle(handle); - - return res == WAIT_OBJECT_0; -} - -HANDLE OrganizerCore::findAndOpenAUSVFSProcess(const std::vector& hiddenList, DWORD preferedParentPid) { - // for practical reasons a querySize of 1 is probably enough, we use a larger query as a heuristics - // to find a more "aesthetic injected processes (attempting to comply to hiddenList and preferedParentPid) - constexpr size_t querySize = 100; - DWORD pids[querySize]; - size_t found = querySize; - if (!::GetVFSProcessList(&found, pids)) { - qWarning() << "Failed seeking USVFS processes : GetVFSProcessList failed?!"; - return INVALID_HANDLE_VALUE; - } - - HANDLE best_match = INVALID_HANDLE_VALUE; - bool best_match_hidden = true; - for (size_t i = 0; i < found; ++i) { - if (pids[i] == GetCurrentProcessId()) - continue; // obviously don't wait for MO process - - HANDLE handle = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pids[i]); - if (handle == INVALID_HANDLE_VALUE) { - qWarning() << "Failed openning USVFS process " << pids[i] << " : OpenProcess failed" << GetLastError(); - continue; - } - - QString pname = QString::fromStdWString(getProcessName(handle)); - bool phidden = false; - for (auto hide : hiddenList) - if (pname.contains(hide, Qt::CaseInsensitive)) - phidden = true; - - bool pprefered = preferedParentPid && getProcessParentID(pids[i]) == preferedParentPid; - - if (best_match == INVALID_HANDLE_VALUE || best_match_hidden || (!phidden && pprefered)) { - if (best_match != INVALID_HANDLE_VALUE) - CloseHandle(best_match); - best_match = handle; - best_match_hidden = phidden; - } - else - CloseHandle(handle); - - if (!phidden && pprefered) - return best_match; - } - - return best_match; -} - -bool OrganizerCore::onAboutToRun( - const std::function &func) -{ - auto conn = m_AboutToRun.connect(func); - return conn.connected(); -} - -bool OrganizerCore::onFinishedRun( - const std::function &func) -{ - auto conn = m_FinishedRun.connect(func); - return conn.connected(); -} - -bool OrganizerCore::onModInstalled( - const std::function &func) -{ - auto conn = m_ModInstalled.connect(func); - return conn.connected(); -} - -void OrganizerCore::refreshModList(bool saveChanges) -{ - // don't lose changes! - if (saveChanges) { - m_CurrentProfile->writeModlistNow(true); - } - ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, - m_PluginContainer, m_Settings.displayForeign(), managedGame()); - - m_CurrentProfile->refreshModStatus(); - - m_ModList.notifyChange(-1); - - refreshDirectoryStructure(); -} - -void OrganizerCore::refreshESPList(bool force) -{ - if (m_DirectoryUpdate) { - // don't mess up the esp list if we're currently updating the directory - // structure - m_PostRefreshTasks.append([=]() { - this->refreshESPList(force); - }); - return; - } - m_CurrentProfile->writeModlist(); - - // clear list - try { - m_PluginList.refresh(m_CurrentProfile->name(), *m_DirectoryStructure, - m_CurrentProfile->getLockedOrderFileName(), force); - } catch (const std::exception &e) { - reportError(tr("Failed to refresh list of esps: %1").arg(e.what())); - } -} - -void OrganizerCore::refreshBSAList() -{ - DataArchives *archives = m_GamePlugin->feature(); - - if (archives != nullptr) { - m_ArchivesInit = false; - - // default archives are the ones enabled outside MO. if the list can't be - // found (which might - // happen if ini files are missing) use hard-coded defaults (preferrably the - // same the game would use) - m_DefaultArchives = archives->archives(m_CurrentProfile); - if (m_DefaultArchives.length() == 0) { - m_DefaultArchives = archives->vanillaArchives(); - } - - m_ActiveArchives.clear(); - - auto iter = enabledArchives(); - m_ActiveArchives = toStringList(iter.begin(), iter.end()); - if (m_ActiveArchives.isEmpty()) { - m_ActiveArchives = m_DefaultArchives; - } - - if (m_UserInterface != nullptr) { - m_UserInterface->updateBSAList(m_DefaultArchives, m_ActiveArchives); - } - - m_ArchivesInit = true; - } -} - -void OrganizerCore::refreshLists() -{ - if ((m_CurrentProfile != nullptr) && m_DirectoryStructure->isPopulated()) { - refreshESPList(true); - refreshBSAList(); - } // no point in refreshing lists if no files have been added to the directory - // tree -} - -void OrganizerCore::updateModActiveState(int index, bool active) -{ - QList modsToUpdate; - modsToUpdate.append(index); - updateModsActiveState(modsToUpdate, active); -} - -void OrganizerCore::updateModsActiveState(const QList &modIndices, bool active) -{ - int enabled = 0; - for (auto index : modIndices) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - QDir dir(modInfo->absolutePath()); - for (const QString &esm : - dir.entryList(QStringList() << "*.esm", QDir::Files)) { - const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esm)); - if (file.get() == nullptr) { - qWarning("failed to activate %s", qUtf8Printable(esm)); - continue; - } - - if (active != m_PluginList.isEnabled(esm) - && file->getAlternatives().empty()) { - m_PluginList.blockSignals(true); - m_PluginList.enableESP(esm, active); - m_PluginList.blockSignals(false); - } - } - - for (const QString &esl : - dir.entryList(QStringList() << "*.esl", QDir::Files)) { - const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esl)); - if (file.get() == nullptr) { - qWarning("failed to activate %s", qUtf8Printable(esl)); - continue; - } - - if (active != m_PluginList.isEnabled(esl) - && file->getAlternatives().empty()) { - m_PluginList.blockSignals(true); - m_PluginList.enableESP(esl, active); - m_PluginList.blockSignals(false); - ++enabled; - } - } - QStringList esps = dir.entryList(QStringList() << "*.esp", QDir::Files); - for (const QString &esp : esps) { - const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esp)); - if (file.get() == nullptr) { - qWarning("failed to activate %s", qUtf8Printable(esp)); - continue; - } - - if (active != m_PluginList.isEnabled(esp) - && file->getAlternatives().empty()) { - m_PluginList.blockSignals(true); - m_PluginList.enableESP(esp, active); - m_PluginList.blockSignals(false); - ++enabled; - } - } - } - if (active && (enabled > 1)) { - MessageDialog::showMessage( - tr("Multiple esps/esls activated, please check that they don't conflict."), - qApp->activeWindow()); - } - m_PluginList.refreshLoadOrder(); - // immediately save affected lists - m_PluginListsWriter.writeImmediately(false); -} - -void OrganizerCore::updateModInDirectoryStructure(unsigned int index, - ModInfo::Ptr modInfo) -{ - QMap allModInfo; - allModInfo[index] = modInfo; - updateModsInDirectoryStructure(allModInfo); -} - -void OrganizerCore::updateModsInDirectoryStructure(QMap modInfo) -{ - for (auto idx : modInfo.keys()) { - // add files of the bsa to the directory structure - m_DirectoryRefresher.addModFilesToStructure( - m_DirectoryStructure, modInfo[idx]->name(), - m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(), - modInfo[idx]->stealFiles()); - } - DirectoryRefresher::cleanStructure(m_DirectoryStructure); - // need to refresh plugin list now so we can activate esps - refreshESPList(true); - // activate all esps of the specified mod so the bsas get activated along with - // it - m_PluginList.blockSignals(true); - updateModsActiveState(modInfo.keys(), true); - m_PluginList.blockSignals(false); - // now we need to refresh the bsa list and save it so there is no confusion - // about what archives are avaiable and active - refreshBSAList(); - if (m_UserInterface != nullptr) { - m_UserInterface->archivesWriter().writeImmediately(false); - } - - std::vector archives = enabledArchives(); - m_DirectoryRefresher.setMods( - m_CurrentProfile->getActiveMods(), - std::set(archives.begin(), archives.end())); - - // finally also add files from bsas to the directory structure - for (auto idx : modInfo.keys()) { - m_DirectoryRefresher.addModBSAToStructure( - m_DirectoryStructure, modInfo[idx]->name(), - m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(), - modInfo[idx]->archives()); - } -} - -void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply) -{ - if (m_PluginContainer != nullptr) { - for (IPluginModPage *modPage : - m_PluginContainer->plugins()) { - ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo(); - if (modPage->handlesDownload(url, reply->url(), *fileInfo)) { - fileInfo->repository = modPage->name(); - m_DownloadManager.addDownload(reply, fileInfo); - return; - } - } - } - - // no mod found that could handle the download. Is it a nexus mod? - if (url.host() == "www.nexusmods.com") { - QString gameName = ""; - int modID = 0; - int fileID = 0; - QRegExp nameExp("www\\.nexusmods\\.com/(\\a+)/"); - if (nameExp.indexIn(url.toString()) != -1) { - gameName = nameExp.cap(1); - } - QRegExp modExp("mods/(\\d+)"); - if (modExp.indexIn(url.toString()) != -1) { - modID = modExp.cap(1).toInt(); - } - QRegExp fileExp("fid=(\\d+)"); - if (fileExp.indexIn(reply->url().toString()) != -1) { - fileID = fileExp.cap(1).toInt(); - } - m_DownloadManager.addDownload(reply, - new ModRepositoryFileInfo(gameName, modID, fileID)); - } else { - if (QMessageBox::question(qApp->activeWindow(), tr("Download?"), - tr("A download has been started but no installed " - "page plugin recognizes it.\n" - "If you download anyway no information (i.e. " - "version) will be associated with the " - "download.\n" - "Continue?"), - QMessageBox::Yes | QMessageBox::No) - == QMessageBox::Yes) { - m_DownloadManager.addDownload(reply, new ModRepositoryFileInfo()); - } - } -} - -ModListSortProxy *OrganizerCore::createModListProxyModel() -{ - ModListSortProxy *result = new ModListSortProxy(m_CurrentProfile, this); - result->setSourceModel(&m_ModList); - return result; -} - -PluginListSortProxy *OrganizerCore::createPluginListProxyModel() -{ - PluginListSortProxy *result = new PluginListSortProxy(this); - result->setSourceModel(&m_PluginList); - return result; -} - -IPluginGame const *OrganizerCore::managedGame() const -{ - return m_GamePlugin; -} - -std::vector OrganizerCore::enabledArchives() -{ - std::vector result; - if (m_ArchiveParsing) { - QFile archiveFile(m_CurrentProfile->getArchivesFileName()); - if (archiveFile.open(QIODevice::ReadOnly)) { - while (!archiveFile.atEnd()) { - result.push_back(QString::fromUtf8(archiveFile.readLine()).trimmed()); - } - archiveFile.close(); - } - } - return result; -} - -void OrganizerCore::refreshDirectoryStructure() -{ - if (!m_DirectoryUpdate) { - m_CurrentProfile->writeModlistNow(true); - - m_DirectoryUpdate = true; - std::vector> activeModList - = m_CurrentProfile->getActiveMods(); - auto archives = enabledArchives(); - m_DirectoryRefresher.setMods( - activeModList, std::set(archives.begin(), archives.end())); - - QTimer::singleShot(0, &m_DirectoryRefresher, SLOT(refresh())); - } -} - -void OrganizerCore::directory_refreshed() -{ - DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure(); - Q_ASSERT(newStructure != m_DirectoryStructure); - if (newStructure != nullptr) { - std::swap(m_DirectoryStructure, newStructure); - delete newStructure; - } else { - // TODO: don't know why this happens, this slot seems to get called twice - // with only one emit - return; - } - m_DirectoryUpdate = false; - - for (int i = 0; i < m_ModList.rowCount(); ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - modInfo->clearCaches(); - } - for (auto task : m_PostRefreshTasks) { - task(); - } - m_PostRefreshTasks.clear(); - - if (m_CurrentProfile != nullptr) { - refreshLists(); - } -} - -void OrganizerCore::profileRefresh() -{ - // have to refresh mods twice (again in refreshModList), otherwise the refresh - // isn't complete. Not sure why - ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, - m_PluginContainer, m_Settings.displayForeign(), managedGame()); - m_CurrentProfile->refreshModStatus(); - - refreshModList(); -} - -void OrganizerCore::modStatusChanged(unsigned int index) -{ - try { - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - if (m_CurrentProfile->modEnabled(index)) { - updateModInDirectoryStructure(index, modInfo); - } else { - updateModActiveState(index, false); - if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { - FilesOrigin &origin - = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - } - if (m_UserInterface != nullptr) { - m_UserInterface->archivesWriter().write(); - } - } - modInfo->clearCaches(); - - for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - int priority = m_CurrentProfile->getModPriority(i); - if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { - // priorities in the directory structure are one higher because data is - // 0 - m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())) - .setPriority(priority + 1); - } - } - m_DirectoryStructure->getFileRegister()->sortOrigins(); - - refreshLists(); - } catch (const std::exception &e) { - reportError(tr("failed to update mod list: %1").arg(e.what())); - } -} - -void OrganizerCore::modStatusChanged(QList index) { - try { - QMap modsToEnable; - QMap modsToDisable; - for (auto idx : index) { - if (m_CurrentProfile->modEnabled(idx)) { - modsToEnable[idx] = ModInfo::getByIndex(idx); - } else { - modsToDisable[idx] = ModInfo::getByIndex(idx); - } - } - if (!modsToEnable.isEmpty()) { - updateModsInDirectoryStructure(modsToEnable); - for (auto modInfo : modsToEnable.values()) { - modInfo->clearCaches(); - } - } - if (!modsToDisable.isEmpty()) { - updateModsActiveState(modsToDisable.keys(), false); - for (auto idx : modsToDisable.keys()) { - if (m_DirectoryStructure->originExists(ToWString(modsToDisable[idx]->name()))) { - FilesOrigin &origin - = m_DirectoryStructure->getOriginByName(ToWString(modsToDisable[idx]->name())); - origin.enable(false); - } - } - if (m_UserInterface != nullptr) { - m_UserInterface->archivesWriter().write(); - } - } - - for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - int priority = m_CurrentProfile->getModPriority(i); - if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { - // priorities in the directory structure are one higher because data is - // 0 - m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())) - .setPriority(priority + 1); - } - } - m_DirectoryStructure->getFileRegister()->sortOrigins(); - - refreshLists(); - } catch (const std::exception &e) { - reportError(tr("failed to update mod list: %1").arg(e.what())); - } -} - -void OrganizerCore::loginSuccessful(bool necessary) -{ - if (necessary) { - MessageDialog::showMessage(tr("login successful"), qApp->activeWindow()); - } - for (QString url : m_PendingDownloads) { - downloadRequestedNXM(url); - } - m_PendingDownloads.clear(); - for (auto task : m_PostLoginTasks) { - task(); - } - - m_PostLoginTasks.clear(); - NexusInterface::instance(m_PluginContainer)->loginCompleted(); -} - -void OrganizerCore::loginSuccessfulUpdate(bool necessary) -{ - if (necessary) { - MessageDialog::showMessage(tr("login successful"), qApp->activeWindow()); - } - m_Updater.startUpdate(); -} - -void OrganizerCore::loginFailed(const QString &message) -{ - if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"), - tr("Login failed, try again?")) - == QMessageBox::Yes) { - if (nexusLogin(true)) { - return; - } - } - - if (!m_PendingDownloads.isEmpty()) { - MessageDialog::showMessage( - tr("login failed: %1. Download will not be associated with an account") - .arg(message), - qApp->activeWindow()); - for (QString url : m_PendingDownloads) { - downloadRequestedNXM(url); - } - m_PendingDownloads.clear(); - } else { - MessageDialog::showMessage(tr("login failed: %1").arg(message), - qApp->activeWindow()); - m_PostLoginTasks.clear(); - } - NexusInterface::instance(m_PluginContainer)->loginCompleted(); -} - -void OrganizerCore::loginFailedUpdate(const QString &message) -{ - MessageDialog::showMessage( - tr("login failed: %1. You need to log-in with Nexus to update MO.") - .arg(message), - qApp->activeWindow()); -} - -void OrganizerCore::syncOverwrite() -{ - unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { - std::vector flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) - != flags.end(); - }); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); - SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure, - qApp->activeWindow()); - if (syncDialog.exec() == QDialog::Accepted) { - syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory())); - modInfo->testValid(); - refreshDirectoryStructure(); - } -} - -QString OrganizerCore::oldMO1HookDll() const -{ - if (auto extender = managedGame()->feature()) { - QString hookdll = QDir::toNativeSeparators( - managedGame()->dataDirectory().absoluteFilePath(extender->PluginPath() + "/hook.dll")); - if (QFile(hookdll).exists()) - return hookdll; - } - return QString(); -} - -std::vector OrganizerCore::activeProblems() const -{ - std::vector problems; - const auto& hookdll = oldMO1HookDll(); - if (!hookdll.isEmpty()) { - // This warning will now be shown every time the problems are checked, which is a bit - // of a "log spam". But since this is a sevre error which will most likely make the - // game crash/freeze/etc. and is very hard to diagnose, this "log spam" will make it - // easier for the user to notice the warning. - qWarning("hook.dll found in game folder: %s", qUtf8Printable(hookdll)); - problems.push_back(PROBLEM_MO1SCRIPTEXTENDERWORKAROUND); - } - return problems; -} - -QString OrganizerCore::shortDescription(unsigned int key) const -{ - switch (key) { - case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: { - return tr("MO1 \"Script Extender\" load mechanism has left hook.dll in your game folder"); - } break; - default: { - return tr("Description missing"); - } break; - } -} - -QString OrganizerCore::fullDescription(unsigned int key) const -{ - switch (key) { - case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: { - return tr("hook.dll has been found in your game folder (right click to copy the full path). " - "This is most likely a leftover of setting the ModOrganizer 1 load mechanism to \"Script Extender\", " - "in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or " - "manually removing the file, otherwise the game is likely to crash and burn.").arg(oldMO1HookDll()); - break; - } - default: { - return tr("Description missing"); - } break; - } -} - -bool OrganizerCore::hasGuidedFix(unsigned int) const -{ - return false; -} - -void OrganizerCore::startGuidedFix(unsigned int) const -{ -} - -bool OrganizerCore::saveCurrentLists() -{ - if (m_DirectoryUpdate) { - qWarning("not saving lists during directory update"); - return false; - } - - try { - savePluginList(); - if (m_UserInterface != nullptr) { - m_UserInterface->archivesWriter().write(); - } - } catch (const std::exception &e) { - reportError(tr("failed to save load order: %1").arg(e.what())); - } - - return true; -} - -void OrganizerCore::savePluginList() -{ - if (m_DirectoryUpdate) { - // delay save till after directory update - m_PostRefreshTasks.append([this]() { - this->savePluginList(); - }); - return; - } - m_PluginList.saveTo(m_CurrentProfile->getLockedOrderFileName(), - m_CurrentProfile->getDeleterFileName(), - m_Settings.hideUncheckedPlugins()); - m_PluginList.saveLoadOrder(*m_DirectoryStructure); -} - -void OrganizerCore::prepareStart() -{ - if (m_CurrentProfile == nullptr) { - return; - } - m_CurrentProfile->writeModlist(); - m_CurrentProfile->createTweakedIniFile(); - saveCurrentLists(); - m_Settings.setupLoadMechanism(); - storeSettings(); -} - -std::vector OrganizerCore::fileMapping(const QString &profileName, - const QString &customOverwrite) -{ - // need to wait until directory structure - while (m_DirectoryUpdate) { - ::Sleep(100); - QCoreApplication::processEvents(); - } - - IPluginGame *game = qApp->property("managed_game").value(); - Profile profile(QDir(m_Settings.getProfileDirectory() + "/" + profileName), - game); - - MappingType result; - - QString dataPath - = QDir::toNativeSeparators(game->dataDirectory().absolutePath()); - - bool overwriteActive = false; - - for (auto mod : profile.getActiveMods()) { - if (std::get<0>(mod).compare("overwrite", Qt::CaseInsensitive) == 0) { - continue; - } - - unsigned int modIndex = ModInfo::getIndex(std::get<0>(mod)); - ModInfo::Ptr modPtr = ModInfo::getByIndex(modIndex); - - bool createTarget = customOverwrite == std::get<0>(mod); - - overwriteActive |= createTarget; - - if (modPtr->isRegular()) { - result.insert(result.end(), {QDir::toNativeSeparators(std::get<1>(mod)), - dataPath, true, createTarget}); - } - } - - if (!overwriteActive && !customOverwrite.isEmpty()) { - throw MyException(tr("The designated write target \"%1\" is not enabled.") - .arg(customOverwrite)); - } - - if (m_CurrentProfile->localSavesEnabled()) { - LocalSavegames *localSaves = game->feature(); - if (localSaves != nullptr) { - MappingType saveMap - = localSaves->mappings(currentProfile()->absolutePath() + "/saves"); - result.reserve(result.size() + saveMap.size()); - result.insert(result.end(), saveMap.begin(), saveMap.end()); - } else { - qWarning("local save games not supported by this game plugin"); - } - } - - result.insert(result.end(), { - QDir::toNativeSeparators(m_Settings.getOverwriteDirectory()), - dataPath, - true, - customOverwrite.isEmpty() - }); - - for (MOBase::IPluginFileMapper *mapper : - m_PluginContainer->plugins()) { - IPlugin *plugin = dynamic_cast(mapper); - if (plugin->isActive()) { - MappingType pluginMap = mapper->mappings(); - result.reserve(result.size() + pluginMap.size()); - result.insert(result.end(), pluginMap.begin(), pluginMap.end()); - } - } - - return result; -} - - -std::vector OrganizerCore::fileMapping( - const QString &dataPath, const QString &relPath, const DirectoryEntry *base, - const DirectoryEntry *directoryEntry, int createDestination) -{ - std::vector result; - - for (FileEntry::Ptr current : directoryEntry->getFiles()) { - bool isArchive = false; - int origin = current->getOrigin(isArchive); - if (isArchive || (origin == 0)) { - continue; - } - - QString originPath - = QString::fromStdWString(base->getOriginByID(origin).getPath()); - QString fileName = QString::fromStdWString(current->getName()); -// QString fileName = ToQString(current->getName()); - QString source = originPath + relPath + fileName; - QString target = dataPath + relPath + fileName; - if (source != target) { - result.push_back({source, target, false, false}); - } - } - - // recurse into subdirectories - std::vector::const_iterator current, end; - directoryEntry->getSubDirectories(current, end); - for (; current != end; ++current) { - int origin = (*current)->anyOrigin(); - - QString originPath - = QString::fromStdWString(base->getOriginByID(origin).getPath()); - QString dirName = QString::fromStdWString((*current)->getName()); - QString source = originPath + relPath + dirName; - QString target = dataPath + relPath + dirName; - - bool writeDestination - = (base == directoryEntry) && (origin == createDestination); - - result.push_back({source, target, true, writeDestination}); - std::vector subRes = fileMapping( - dataPath, relPath + dirName + "\\", base, *current, createDestination); - result.insert(result.end(), subRes.begin(), subRes.end()); - } - return result; -} +#include "organizercore.h" + +#include "delayedfilewriter.h" +#include "guessedvalue.h" +#include "imodinterface.h" +#include "imoinfo.h" +#include "iplugingame.h" +#include "iuserinterface.h" +#include "loadmechanism.h" +#include "messagedialog.h" +#include "modlistsortproxy.h" +#include "modrepositoryfileinfo.h" +#include "nexusinterface.h" +#include "plugincontainer.h" +#include "pluginlistsortproxy.h" +#include "profile.h" +#include "logbuffer.h" +#include "credentialsdialog.h" +#include "filedialogmemory.h" +#include "modinfodialog.h" +#include "spawn.h" +#include "syncoverwritedialog.h" +#include "nxmaccessmanager.h" +#include +#include +#include +#include +#include +#include +#include +#include "appconfig.h" +#include +#include +#include "lockeddialog.h" +#include "instancemanager.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include // for qUtf8Printable, etc + +#include +#include +#include +#include // for _tcsicmp + +#include +#include +#include // for memset, wcsrchr + +#include +#include +#include +#include +#include +#include //for wstring +#include +#include + + +using namespace MOShared; +using namespace MOBase; + +//static +CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None; + +static bool isOnline() +{ + QList interfaces = QNetworkInterface::allInterfaces(); + + bool connected = false; + for (auto iter = interfaces.begin(); iter != interfaces.end() && !connected; + ++iter) { + if ((iter->flags() & QNetworkInterface::IsUp) + && (iter->flags() & QNetworkInterface::IsRunning) + && !(iter->flags() & QNetworkInterface::IsLoopBack)) { + auto addresses = iter->addressEntries(); + if (addresses.count() == 0) { + continue; + } + qDebug("interface %s seems to be up (address: %s)", + qUtf8Printable(iter->humanReadableName()), + qUtf8Printable(addresses[0].ip().toString())); + connected = true; + } + } + + return connected; +} + +static bool renameFile(const QString &oldName, const QString &newName, + bool overwrite = true) +{ + if (overwrite && QFile::exists(newName)) { + QFile::remove(newName); + } + return QFile::rename(oldName, newName); +} + +static std::wstring getProcessName(HANDLE process) +{ + wchar_t buffer[MAX_PATH]; + wchar_t *fileName = L"unknown"; + + if (process == nullptr) return fileName; + + if (::GetProcessImageFileNameW(process, buffer, MAX_PATH) != 0) { + fileName = wcsrchr(buffer, L'\\'); + if (fileName == nullptr) { + fileName = buffer; + } + else { + fileName += 1; + } + } + + return fileName; +} + +// Get parent PID for the given process, return 0 on failure +static DWORD getProcessParentID(DWORD pid) +{ + HANDLE th = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 pe = { 0 }; + pe.dwSize = sizeof(PROCESSENTRY32); + + DWORD res = 0; + if (Process32First(th, &pe)) + do { + if (pe.th32ProcessID == pid) { + res = pe.th32ParentProcessID; + break; + } + } while (Process32Next(th, &pe)); + + CloseHandle(th); + + return res; +} + +static void startSteam(QWidget *widget) +{ + QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam", + QSettings::NativeFormat); + QString exe = steamSettings.value("SteamExe", "").toString(); + if (!exe.isEmpty()) { + exe = QString("\"%1\"").arg(exe); + // See if username and password supplied. If so, pass them into steam. + QStringList args; + QString username; + QString password; + if (Settings::instance().getSteamLogin(username, password)) { + args << "-login"; + args << username; + if (password != "") { + args << password; + } + } + if (!QProcess::startDetached(exe, args)) { + reportError(QObject::tr("Failed to start \"%1\"").arg(exe)); + } else { + QMessageBox::information( + widget, QObject::tr("Waiting"), + QObject::tr("Please press OK once you're logged into steam.")); + } + } +} + +template +QStringList toStringList(InputIterator current, InputIterator end) +{ + QStringList result; + for (; current != end; ++current) { + result.append(*current); + } + return result; +} + +bool checkService() +{ + SC_HANDLE serviceManagerHandle = NULL; + SC_HANDLE serviceHandle = NULL; + LPSERVICE_STATUS_PROCESS serviceStatus = NULL; + LPQUERY_SERVICE_CONFIG serviceConfig = NULL; + bool serviceRunning = true; + + DWORD bytesNeeded; + + try { + serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); + if (!serviceManagerHandle) { + qWarning("failed to open service manager (query status) (error %d)", GetLastError()); + throw 1; + } + + serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); + if (!serviceHandle) { + qWarning("failed to open EventLog service (query status) (error %d)", GetLastError()); + throw 2; + } + + if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded) + || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { + qWarning("failed to get size of service config (error %d)", GetLastError()); + throw 3; + } + + DWORD serviceConfigSize = bytesNeeded; + serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize); + if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) { + qWarning("failed to query service config (error %d)", GetLastError()); + throw 4; + } + + if (serviceConfig->dwStartType == SERVICE_DISABLED) { + qCritical("Windows Event Log service is disabled!"); + serviceRunning = false; + } + + if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded) + || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { + qWarning("failed to get size of service status (error %d)", GetLastError()); + throw 5; + } + + DWORD serviceStatusSize = bytesNeeded; + serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize); + if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) { + qWarning("failed to query service status (error %d)", GetLastError()); + throw 6; + } + + if (serviceStatus->dwCurrentState != SERVICE_RUNNING) { + qCritical("Windows Event Log service is not running"); + serviceRunning = false; + } + } + catch (int e) { + UNUSED_VAR(e); + serviceRunning = false; + } + + if (serviceStatus) { + LocalFree(serviceStatus); + } + if (serviceConfig) { + LocalFree(serviceConfig); + } + if (serviceHandle) { + CloseServiceHandle(serviceHandle); + } + if (serviceManagerHandle) { + CloseServiceHandle(serviceManagerHandle); + } + + return serviceRunning; +} + +OrganizerCore::OrganizerCore(const QSettings &initSettings) + : m_UserInterface(nullptr) + , m_PluginContainer(nullptr) + , m_GameName() + , m_CurrentProfile(nullptr) + , m_Settings(initSettings) + , m_Updater(NexusInterface::instance(m_PluginContainer)) + , m_AboutToRun() + , m_FinishedRun() + , m_ModInstalled() + , m_ModList(m_PluginContainer, this) + , m_PluginList(this) + , m_DirectoryRefresher() + , m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0)) + , m_DownloadManager(NexusInterface::instance(m_PluginContainer), this) + , m_InstallationManager() + , m_RefresherThread() + , m_DirectoryUpdate(false) + , m_ArchivesInit(false) + , m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this)) +{ + m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); + m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers()); + + NexusInterface::instance(m_PluginContainer)->setCacheDirectory(m_Settings.getCacheDirectory()); + NexusInterface::instance(m_PluginContainer)->setNMMVersion(m_Settings.getNMMVersion()); + + MOBase::QuestionBoxMemory::init(initSettings.fileName()); + + m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); + m_InstallationManager.setDownloadDirectory(m_Settings.getDownloadDirectory()); + + connect(&m_DownloadManager, SIGNAL(downloadSpeed(QString, int)), this, + SLOT(downloadSpeed(QString, int))); + connect(&m_DirectoryRefresher, SIGNAL(refreshed()), this, + SLOT(directory_refreshed())); + + connect(&m_ModList, SIGNAL(removeOrigin(QString)), this, + SLOT(removeOrigin(QString))); + + connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(), + SIGNAL(loginSuccessful(bool)), this, SLOT(loginSuccessful(bool))); + connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(), + SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); + + // This seems awfully imperative + connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), + &m_Settings, SLOT(managedGameChanged(MOBase::IPluginGame const *))); + connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), + &m_DownloadManager, + SLOT(managedGameChanged(MOBase::IPluginGame const *))); + connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), + &m_PluginList, SLOT(managedGameChanged(MOBase::IPluginGame const *))); + + connect(&m_PluginList, &PluginList::writePluginsList, &m_PluginListsWriter, + &DelayedFileWriterBase::write); + + // make directory refresher run in a separate thread + m_RefresherThread.start(); + m_DirectoryRefresher.moveToThread(&m_RefresherThread); +} + +OrganizerCore::~OrganizerCore() +{ + m_RefresherThread.exit(); + m_RefresherThread.wait(); + + prepareStart(); + + // profile has to be cleaned up before the modinfo-buffer is cleared + delete m_CurrentProfile; + m_CurrentProfile = nullptr; + + ModInfo::clear(); + LogBuffer::cleanQuit(); + m_ModList.setProfile(nullptr); + // NexusInterface::instance()->cleanup(); + + delete m_DirectoryStructure; +} + +QString OrganizerCore::commitSettings(const QString &iniFile) +{ + if (!shellRename(iniFile + ".new", iniFile, true, qApp->activeWindow())) { + DWORD err = ::GetLastError(); + // make a second attempt using qt functions but if that fails print the + // error from the first attempt + if (!renameFile(iniFile + ".new", iniFile)) { + return windowsErrorString(err); + } + } + return QString(); +} + +QSettings::Status OrganizerCore::storeSettings(const QString &fileName) +{ + QSettings settings(fileName, QSettings::IniFormat); + if (m_UserInterface != nullptr) { + m_UserInterface->storeSettings(settings); + } + if (m_CurrentProfile != nullptr) { + settings.setValue("selected_profile", + m_CurrentProfile->name().toUtf8().constData()); + } + + settings.remove("customExecutables"); + settings.beginWriteArray("customExecutables"); + std::vector::const_iterator current, end; + m_ExecutablesList.getExecutables(current, end); + int count = 0; + for (; current != end; ++current) { + const Executable &item = *current; + settings.setArrayIndex(count++); + settings.setValue("title", item.m_Title); + settings.setValue("custom", item.isCustom()); + settings.setValue("toolbar", item.isShownOnToolbar()); + settings.setValue("ownicon", item.usesOwnIcon()); + if (item.isCustom()) { + settings.setValue("binary", item.m_BinaryInfo.absoluteFilePath()); + settings.setValue("arguments", item.m_Arguments); + settings.setValue("workingDirectory", item.m_WorkingDirectory); + settings.setValue("steamAppID", item.m_SteamAppID); + } + } + settings.endArray(); + + FileDialogMemory::save(settings); + + settings.sync(); + return settings.status(); +} + +void OrganizerCore::storeSettings() +{ + QString iniFile = qApp->property("dataPath").toString() + "/" + + QString::fromStdWString(AppConfig::iniFileName()); + if (QFileInfo(iniFile).exists()) { + if (!shellCopy(iniFile, iniFile + ".new", true, qApp->activeWindow())) { + QMessageBox::critical( + qApp->activeWindow(), tr("Failed to write settings"), + tr("An error occured trying to update MO settings to %1: %2") + .arg(iniFile, windowsErrorString(::GetLastError()))); + return; + } + } + + QString writeTarget = iniFile + ".new"; + + QSettings::Status result = storeSettings(writeTarget); + + if (result == QSettings::NoError) { + QString errMsg = commitSettings(iniFile); + if (!errMsg.isEmpty()) { + qWarning("settings file not writable, may be locked by another " + "application, trying direct write"); + writeTarget = iniFile; + result = storeSettings(iniFile); + } + } + if (result != QSettings::NoError) { + QString reason = result == QSettings::AccessError + ? tr("File is write protected") + : result == QSettings::FormatError + ? tr("Invalid file format (probably a bug)") + : tr("Unknown error %1").arg(result); + QMessageBox::critical( + qApp->activeWindow(), tr("Failed to write settings"), + tr("An error occured trying to write back MO settings to %1: %2") + .arg(writeTarget, reason)); + } +} + +bool OrganizerCore::testForSteam() +{ + size_t currentSize = 1024; + std::unique_ptr processIDs; + DWORD bytesReturned; + bool success = false; + while (!success) { + processIDs.reset(new DWORD[currentSize]); + if (!::EnumProcesses(processIDs.get(), + static_cast(currentSize) * sizeof(DWORD), + &bytesReturned)) { + qWarning("failed to determine if steam is running"); + return true; + } + if (bytesReturned == (currentSize * sizeof(DWORD))) { + // maximum size used, list probably truncated + currentSize *= 2; + } else { + success = true; + } + } + TCHAR processName[MAX_PATH]; + for (unsigned int i = 0; i < bytesReturned / sizeof(DWORD); ++i) { + memset(processName, '\0', sizeof(TCHAR) * MAX_PATH); + if (processIDs[i] != 0) { + HANDLE process = ::OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processIDs[i]); + + if (process != nullptr) { + + ON_BLOCK_EXIT([&]() { + if (process != INVALID_HANDLE_VALUE) + ::CloseHandle(process); + }); + + HMODULE module; + DWORD ignore; + + // first module in a process is always the binary + if (EnumProcessModules(process, &module, sizeof(HMODULE) * 1, + &ignore)) { + ::GetModuleBaseName(process, module, processName, MAX_PATH); + if ((_tcsicmp(processName, TEXT("steam.exe")) == 0) + || (_tcsicmp(processName, TEXT("steamservice.exe")) == 0)) { + return true; + } + } + } + } + } + + return false; +} + +void OrganizerCore::updateExecutablesList(QSettings &settings) +{ + if (m_PluginContainer == nullptr) { + qCritical("can't update executables list now"); + return; + } + + m_ExecutablesList.init(managedGame()); + + qDebug("setting up configured executables"); + + int numCustomExecutables = settings.beginReadArray("customExecutables"); + for (int i = 0; i < numCustomExecutables; ++i) { + settings.setArrayIndex(i); + + Executable::Flags flags; + if (settings.value("custom", true).toBool()) + flags |= Executable::CustomExecutable; + if (settings.value("toolbar", false).toBool()) + flags |= Executable::ShowInToolbar; + if (settings.value("ownicon", false).toBool()) + flags |= Executable::UseApplicationIcon; + + m_ExecutablesList.addExecutable( + settings.value("title").toString(), settings.value("binary").toString(), + settings.value("arguments").toString(), + settings.value("workingDirectory", "").toString(), + settings.value("steamAppID", "").toString(), flags); + } + + settings.endArray(); + + // TODO this has nothing to do with executables list move to an appropriate + // function! + ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, + m_PluginContainer, m_Settings.displayForeign(), managedGame()); +} + +void OrganizerCore::setUserInterface(IUserInterface *userInterface, + QWidget *widget) +{ + storeSettings(); + + m_UserInterface = userInterface; + + if (widget != nullptr) { + connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), widget, + SLOT(modlistChanged(QModelIndex, int))); + connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), widget, + SLOT(modlistChanged(QModelIndexList, int))); + connect(&m_ModList, SIGNAL(showMessage(QString)), widget, + SLOT(showMessage(QString))); + connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), widget, + SLOT(modRenamed(QString, QString))); + connect(&m_ModList, SIGNAL(modUninstalled(QString)), widget, + SLOT(modRemoved(QString))); + connect(&m_ModList, SIGNAL(removeSelectedMods()), widget, + SLOT(removeMod_clicked())); + connect(&m_ModList, SIGNAL(clearOverwrite()), widget, + SLOT(clearOverwrite())); + connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), widget, + SLOT(displayColumnSelection(QPoint))); + connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), widget, + SLOT(fileMoved(QString, QString, QString))); + connect(&m_ModList, SIGNAL(modorder_changed()), widget, + SLOT(modorder_changed())); + connect(&m_PluginList, SIGNAL(writePluginsList()), widget, + SLOT(esplist_changed())); + connect(&m_PluginList, SIGNAL(esplist_changed()), widget, + SLOT(esplist_changed())); + connect(&m_DownloadManager, SIGNAL(showMessage(QString)), widget, + SLOT(showMessage(QString))); + } + + m_InstallationManager.setParentWidget(widget); + m_Updater.setUserInterface(widget); + + if (userInterface != nullptr) { + // this currently wouldn't work reliably if the ui isn't initialized yet to + // display the result + if (isOnline() && !m_Settings.offlineMode()) { + m_Updater.testForUpdate(); + } else { + qDebug("user doesn't seem to be connected to the internet"); + } + } +} + +void OrganizerCore::connectPlugins(PluginContainer *container) +{ + m_DownloadManager.setSupportedExtensions( + m_InstallationManager.getSupportedExtensions()); + m_PluginContainer = container; + m_Updater.setPluginContainer(m_PluginContainer); + m_DownloadManager.setPluginContainer(m_PluginContainer); + m_ModList.setPluginContainer(m_PluginContainer); + + if (!m_GameName.isEmpty()) { + m_GamePlugin = m_PluginContainer->managedGame(m_GameName); + emit managedGameChanged(m_GamePlugin); + } +} + +void OrganizerCore::disconnectPlugins() +{ + m_AboutToRun.disconnect_all_slots(); + m_FinishedRun.disconnect_all_slots(); + m_ModInstalled.disconnect_all_slots(); + m_ModList.disconnectSlots(); + m_PluginList.disconnectSlots(); + m_Updater.setPluginContainer(nullptr); + m_DownloadManager.setPluginContainer(nullptr); + m_ModList.setPluginContainer(nullptr); + + m_Settings.clearPlugins(); + m_GamePlugin = nullptr; + m_PluginContainer = nullptr; +} + +void OrganizerCore::setManagedGame(MOBase::IPluginGame *game) +{ + m_GameName = game->gameName(); + m_GamePlugin = game; + qApp->setProperty("managed_game", QVariant::fromValue(m_GamePlugin)); + emit managedGameChanged(m_GamePlugin); +} + +Settings &OrganizerCore::settings() +{ + return m_Settings; +} + +bool OrganizerCore::nexusApi(bool retry) +{ + NXMAccessManager *accessManager + = NexusInterface::instance(m_PluginContainer)->getAccessManager(); + + if ((accessManager->validateAttempted() || accessManager->validated()) + && !retry) { + // previous attempt, maybe even successful + return false; + } else { + QString apiKey; + if (!retry && m_Settings.getNexusApiKey(apiKey)) { + // credentials stored or user entered them manually + qDebug("attempt to verify nexus api key"); + accessManager->apiCheck(apiKey); + return true; + } else { + // no credentials stored and user didn't enter them + accessManager->refuseValidation(); + return false; + } + } +} + +void OrganizerCore::startMOUpdate() +{ + if (nexusApi()) { + m_PostLoginTasks.append([&]() { m_Updater.startUpdate(); }); + } else { + m_Updater.startUpdate(); + } +} + +void OrganizerCore::downloadRequestedNXM(const QString &url) +{ + qDebug("download requested: %s", qUtf8Printable(url)); + if (nexusApi()) { + m_PendingDownloads.append(url); + } else { + m_DownloadManager.addNXMDownload(url); + } +} + +void OrganizerCore::externalMessage(const QString &message) +{ + if (MOShortcut moshortcut{ message } ) { + if(moshortcut.hasExecutable()) + runShortcut(moshortcut); + } + else if (isNxmLink(message)) { + MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); + downloadRequestedNXM(message); + } +} + +void OrganizerCore::downloadRequested(QNetworkReply *reply, QString gameName, int modID, + const QString &fileName) +{ + try { + if (m_DownloadManager.addDownload(reply, QStringList(), fileName, gameName, modID, 0, + new ModRepositoryFileInfo(gameName, modID))) { + MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); + } + } catch (const std::exception &e) { + MessageDialog::showMessage(tr("Download failed"), qApp->activeWindow()); + qCritical("exception starting download: %s", e.what()); + } +} + +void OrganizerCore::removeOrigin(const QString &name) +{ + FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(name)); + origin.enable(false); + refreshLists(); +} + +void OrganizerCore::downloadSpeed(const QString &serverName, int bytesPerSecond) +{ + m_Settings.setDownloadSpeed(serverName, bytesPerSecond); +} + +InstallationManager *OrganizerCore::installationManager() +{ + return &m_InstallationManager; +} + +bool OrganizerCore::createDirectory(const QString &path) { + if (!QDir(path).exists() && !QDir().mkpath(path)) { + QMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("Failed to create \"%1\". Your user " + "account probably lacks permission.") + .arg(QDir::toNativeSeparators(path))); + return false; + } else { + return true; + } +} + +bool OrganizerCore::bootstrap() { + return createDirectory(m_Settings.getProfileDirectory()) && + createDirectory(m_Settings.getModDirectory()) && + createDirectory(m_Settings.getDownloadDirectory()) && + createDirectory(m_Settings.getOverwriteDirectory()) && + createDirectory(QString::fromStdWString(crashDumpsPath())) && cycleDiagnostics(); +} + +void OrganizerCore::createDefaultProfile() +{ + QString profilesPath = settings().getProfileDirectory(); + if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size() + == 0) { + Profile newProf("Default", managedGame(), false); + } +} + +void OrganizerCore::prepareVFS() +{ + m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString())); +} + +void OrganizerCore::updateVFSParams(int logLevel, int crashDumpsType, QString executableBlacklist) { + setGlobalCrashDumpsType(crashDumpsType); + m_USVFS.updateParams(logLevel, crashDumpsType, executableBlacklist); +} + +bool OrganizerCore::cycleDiagnostics() { + if (int maxDumps = settings().crashDumpsMax()) + removeOldFiles(QString::fromStdWString(crashDumpsPath()), "*.dmp", maxDumps, QDir::Time|QDir::Reversed); + return true; +} + +//static +void OrganizerCore::setGlobalCrashDumpsType(int crashDumpsType) { + m_globalCrashDumpsType = ::crashDumpsType(crashDumpsType); +} + +//static +std::wstring OrganizerCore::crashDumpsPath() { + return ( + qApp->property("dataPath").toString() + "/" + + QString::fromStdWString(AppConfig::dumpsDir()) + ).toStdWString(); +} + +bool OrganizerCore::getArchiveParsing() const +{ + return m_ArchiveParsing; +} + +void OrganizerCore::setArchiveParsing(const bool archiveParsing) +{ + m_ArchiveParsing = archiveParsing; +} + +void OrganizerCore::setCurrentProfile(const QString &profileName) +{ + if ((m_CurrentProfile != nullptr) + && (profileName == m_CurrentProfile->name())) { + return; + } + + QDir profileBaseDir(settings().getProfileDirectory()); + QString profileDir = profileBaseDir.absoluteFilePath(profileName); + + if (!QDir(profileDir).exists()) { + // selected profile doesn't exist. Ensure there is at least one profile, + // then pick any one + createDefaultProfile(); + + profileDir = profileBaseDir.absoluteFilePath( + profileBaseDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot).at(0)); + } + + Profile *newProfile = new Profile(QDir(profileDir), managedGame()); + + delete m_CurrentProfile; + m_CurrentProfile = newProfile; + m_ModList.setProfile(newProfile); + + if (m_CurrentProfile->invalidationActive(nullptr)) { + m_CurrentProfile->activateInvalidation(); + } else { + m_CurrentProfile->deactivateInvalidation(); + } + + connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this, SLOT(modStatusChanged(uint))); + connect(m_CurrentProfile, SIGNAL(modStatusChanged(QList)), this, SLOT(modStatusChanged(QList))); + refreshDirectoryStructure(); + + //This line is not actually needed and was only added to allow some + //outside detection of Mo2 profile change. (like BaobobMiller utility) + if (m_CurrentProfile != nullptr) { + settings().directInterface().setValue("selected_profile", + m_CurrentProfile->name().toUtf8().constData()); + } +} + +MOBase::IModRepositoryBridge *OrganizerCore::createNexusBridge() const +{ + return new NexusBridge(m_PluginContainer); +} + +QString OrganizerCore::profileName() const +{ + if (m_CurrentProfile != nullptr) { + return m_CurrentProfile->name(); + } else { + return ""; + } +} + +QString OrganizerCore::profilePath() const +{ + if (m_CurrentProfile != nullptr) { + return m_CurrentProfile->absolutePath(); + } else { + return ""; + } +} + +QString OrganizerCore::downloadsPath() const +{ + return QDir::fromNativeSeparators(m_Settings.getDownloadDirectory()); +} + +QString OrganizerCore::overwritePath() const +{ + return QDir::fromNativeSeparators(m_Settings.getOverwriteDirectory()); +} + +QString OrganizerCore::basePath() const +{ + return QDir::fromNativeSeparators(m_Settings.getBaseDirectory()); +} + +MOBase::VersionInfo OrganizerCore::appVersion() const +{ + return m_Updater.getVersion(); +} + +MOBase::IModInterface *OrganizerCore::getMod(const QString &name) const +{ + unsigned int index = ModInfo::getIndex(name); + return index == UINT_MAX ? nullptr : ModInfo::getByIndex(index).data(); +} + +MOBase::IPluginGame *OrganizerCore::getGame(const QString &name) const +{ + for (IPluginGame *game : m_PluginContainer->plugins()) { + if (game->gameShortName().compare(name, Qt::CaseInsensitive) == 0) + return game; + } + return nullptr; +} + +MOBase::IModInterface *OrganizerCore::createMod(GuessedValue &name) +{ + bool merge = false; + if (!m_InstallationManager.testOverwrite(name, &merge)) { + return nullptr; + } + + m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); + + QString targetDirectory + = QDir::fromNativeSeparators(m_Settings.getModDirectory()) + .append("/") + .append(name); + + QSettings settingsFile(targetDirectory + "/meta.ini", QSettings::IniFormat); + + if (!merge) { + settingsFile.setValue("modid", 0); + settingsFile.setValue("version", ""); + settingsFile.setValue("newestVersion", ""); + settingsFile.setValue("category", 0); + settingsFile.setValue("installationFile", ""); + + settingsFile.remove("installedFiles"); + settingsFile.beginWriteArray("installedFiles", 0); + settingsFile.endArray(); + } + + return ModInfo::createFrom(m_PluginContainer, m_GamePlugin, QDir(targetDirectory), &m_DirectoryStructure) + .data(); +} + +bool OrganizerCore::removeMod(MOBase::IModInterface *mod) +{ + unsigned int index = ModInfo::getIndex(mod->name()); + if (index == UINT_MAX) { + return mod->remove(); + } else { + return ModInfo::removeMod(index); + } +} + +void OrganizerCore::modDataChanged(MOBase::IModInterface *) +{ + refreshModList(false); +} + +QVariant OrganizerCore::pluginSetting(const QString &pluginName, + const QString &key) const +{ + return m_Settings.pluginSetting(pluginName, key); +} + +void OrganizerCore::setPluginSetting(const QString &pluginName, + const QString &key, const QVariant &value) +{ + m_Settings.setPluginSetting(pluginName, key, value); +} + +QVariant OrganizerCore::persistent(const QString &pluginName, + const QString &key, + const QVariant &def) const +{ + return m_Settings.pluginPersistent(pluginName, key, def); +} + +void OrganizerCore::setPersistent(const QString &pluginName, const QString &key, + const QVariant &value, bool sync) +{ + m_Settings.setPluginPersistent(pluginName, key, value, sync); +} + +QString OrganizerCore::pluginDataPath() const +{ + return qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()) + + "/data"; +} + +MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName, + const QString &initModName) +{ + if (m_CurrentProfile == nullptr) { + return nullptr; + } + + if (m_InstallationManager.isRunning()) { + QMessageBox::information( + qApp->activeWindow(), tr("Installation cancelled"), + tr("Another installation is currently in progress."), QMessageBox::Ok); + return nullptr; + } + + bool hasIniTweaks = false; + GuessedValue modName; + if (!initModName.isEmpty()) { + modName.update(initModName, GUESS_USER); + } + m_CurrentProfile->writeModlistNow(); + m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); + if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { + MessageDialog::showMessage(tr("Installation successful"), + qApp->activeWindow()); + refreshModList(); + + int modIndex = ModInfo::getIndex(modName); + if (modIndex != UINT_MAX) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if (hasIniTweaks && (m_UserInterface != nullptr) + && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), + tr("This mod contains ini tweaks. Do you " + "want to configure them now?"), + QMessageBox::Yes | QMessageBox::No) + == QMessageBox::Yes)) { + m_UserInterface->displayModInformation(modInfo, modIndex, + ModInfoDialog::TAB_INIFILES); + } + m_ModInstalled(modName); + m_DownloadManager.markInstalled(fileName); + emit modInstalled(modName); + return modInfo.data(); + } else { + reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); + } + } else if (m_InstallationManager.wasCancelled()) { + QMessageBox::information(qApp->activeWindow(), tr("Installation cancelled"), + tr("The mod was not installed completely."), + QMessageBox::Ok); + } + return nullptr; +} + +void OrganizerCore::installDownload(int index) +{ + if (m_InstallationManager.isRunning()) { + QMessageBox::information( + qApp->activeWindow(), tr("Installation cancelled"), + tr("Another installation is currently in progress."), QMessageBox::Ok); + return; + } + + try { + QString fileName = m_DownloadManager.getFilePath(index); + QString gameName = m_DownloadManager.getGameName(index); + int modID = m_DownloadManager.getModID(index); + int fileID = m_DownloadManager.getFileInfo(index)->fileID; + GuessedValue modName; + + // see if there already are mods with the specified mod id + if (modID != 0) { + std::vector modInfo = ModInfo::getByModID(gameName, modID); + for (auto iter = modInfo.begin(); iter != modInfo.end(); ++iter) { + std::vector flags = (*iter)->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) + == flags.end()) { + modName.update((*iter)->name(), GUESS_PRESET); + (*iter)->saveMeta(); + } + } + } + + m_CurrentProfile->writeModlistNow(); + + bool hasIniTweaks = false; + m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); + if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { + MessageDialog::showMessage(tr("Installation successful"), + qApp->activeWindow()); + refreshModList(); + + int modIndex = ModInfo::getIndex(modName); + if (modIndex != UINT_MAX) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + modInfo->addInstalledFile(modID, fileID); + + if (hasIniTweaks && m_UserInterface != nullptr + && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), + tr("This mod contains ini tweaks. Do you " + "want to configure them now?"), + QMessageBox::Yes | QMessageBox::No) + == QMessageBox::Yes)) { + m_UserInterface->displayModInformation(modInfo, modIndex, + ModInfoDialog::TAB_INIFILES); + } + + m_ModInstalled(modName); + } else { + reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); + } + m_DownloadManager.markInstalled(index); + + emit modInstalled(modName); + } else if (m_InstallationManager.wasCancelled()) { + QMessageBox::information( + qApp->activeWindow(), tr("Installation cancelled"), + tr("The mod was not installed completely."), QMessageBox::Ok); + } + } catch (const std::exception &e) { + reportError(e.what()); + } +} + +QString OrganizerCore::resolvePath(const QString &fileName) const +{ + if (m_DirectoryStructure == nullptr) { + return QString(); + } + const FileEntry::Ptr file + = m_DirectoryStructure->searchFile(ToWString(fileName), nullptr); + if (file.get() != nullptr) { + return ToQString(file->getFullPath()); + } else { + return QString(); + } +} + +QStringList OrganizerCore::listDirectories(const QString &directoryName) const +{ + QStringList result; + DirectoryEntry *dir = m_DirectoryStructure; + if (!directoryName.isEmpty()) + dir = dir->findSubDirectoryRecursive(ToWString(directoryName)); + if (dir != nullptr) { + std::vector::iterator current, end; + dir->getSubDirectories(current, end); + for (; current != end; ++current) { + result.append(ToQString((*current)->getName())); + } + } + return result; +} + +QStringList OrganizerCore::findFiles( + const QString &path, + const std::function &filter) const +{ + QStringList result; + DirectoryEntry *dir = m_DirectoryStructure; + if (!path.isEmpty()) + dir = dir->findSubDirectoryRecursive(ToWString(path)); + if (dir != nullptr) { + std::vector files = dir->getFiles(); + foreach (FileEntry::Ptr file, files) { + if (filter(ToQString(file->getFullPath()))) { + result.append(ToQString(file->getFullPath())); + } + } + } else { + qWarning("directory not found: %1", qUtf8Printable(path)); + } + return result; +} + +QStringList OrganizerCore::getFileOrigins(const QString &fileName) const +{ + QStringList result; + const FileEntry::Ptr file = m_DirectoryStructure->searchFile( + ToWString(QFileInfo(fileName).fileName()), nullptr); + + if (file.get() != nullptr) { + result.append(ToQString( + m_DirectoryStructure->getOriginByID(file->getOrigin()).getName())); + foreach (auto i, file->getAlternatives()) { + result.append( + ToQString(m_DirectoryStructure->getOriginByID(i.first).getName())); + } + } else { + qWarning("file not found: %1", qUtf8Printable(fileName)); + } + return result; +} + +QList OrganizerCore::findFileInfos( + const QString &path, + const std::function &filter) + const +{ + QList result; + DirectoryEntry *dir = m_DirectoryStructure; + if (!path.isEmpty()) + dir = dir->findSubDirectoryRecursive(ToWString(path)); + if (dir != nullptr) { + std::vector files = dir->getFiles(); + foreach (FileEntry::Ptr file, files) { + IOrganizer::FileInfo info; + info.filePath = ToQString(file->getFullPath()); + bool fromArchive = false; + info.origins.append(ToQString( + m_DirectoryStructure->getOriginByID(file->getOrigin(fromArchive)) + .getName())); + info.archive = fromArchive ? ToQString(file->getArchive().first) : ""; + foreach (auto idx, file->getAlternatives()) { + info.origins.append( + ToQString(m_DirectoryStructure->getOriginByID(idx.first).getName())); + } + + if (filter(info)) { + result.append(info); + } + } + } + return result; +} + +DownloadManager *OrganizerCore::downloadManager() +{ + return &m_DownloadManager; +} + +PluginList *OrganizerCore::pluginList() +{ + return &m_PluginList; +} + +ModList *OrganizerCore::modList() +{ + return &m_ModList; +} + +QStringList OrganizerCore::modsSortedByProfilePriority() const +{ + QStringList res; + for (int i = currentProfile()->getPriorityMinimum(); + i < currentProfile()->getPriorityMinimum() + (int)currentProfile()->numRegularMods(); + ++i) { + int modIndex = currentProfile()->modIndexByPriority(i); + auto modInfo = ModInfo::getByIndex(modIndex); + if (!modInfo->hasFlag(ModInfo::FLAG_OVERWRITE) && + !modInfo->hasFlag(ModInfo::FLAG_BACKUP)) { + res.push_back(ModInfo::getByIndex(modIndex)->name()); + } + } + return res; +} + +void OrganizerCore::spawnBinary(const QFileInfo &binary, + const QString &arguments, + const QDir ¤tDirectory, + const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries) +{ + DWORD processExitCode = 0; + HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, customOverwrite, forcedLibraries, &processExitCode); + if (processHandle != INVALID_HANDLE_VALUE) { + refreshDirectoryStructure(); + // need to remove our stored load order because it may be outdated if a foreign tool changed the + // file time. After removing that file, refreshESPList will use the file time as the order + if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) { + qDebug("removing loadorder.txt"); + QFile::remove(m_CurrentProfile->getLoadOrderFileName()); + } + refreshDirectoryStructure(); + + refreshESPList(true); + savePluginList(); + + //These callbacks should not fiddle with directoy structure and ESPs. + m_FinishedRun(binary.absoluteFilePath(), processExitCode); + } +} + +HANDLE OrganizerCore::spawnBinaryDirect(const QFileInfo &binary, + const QString &arguments, + const QString &profileName, + const QDir ¤tDirectory, + const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries, + LPDWORD exitCode) +{ + HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); + if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) { + std::unique_ptr dlg; + ILockedWaitingForProcess* uilock = nullptr; + + if (m_UserInterface != nullptr) { + uilock = m_UserInterface->lock(); + } + else { + // i.e. when running command line shortcuts there is no m_UserInterface + dlg.reset(new LockedDialog); + dlg->show(); + dlg->setEnabled(true); + uilock = dlg.get(); + } + + ON_BLOCK_EXIT([&]() { + if (m_UserInterface != nullptr) { + m_UserInterface->unlock(); + } }); + + DWORD ignoreExitCode; + waitForProcessCompletion(processHandle, exitCode ? exitCode : &ignoreExitCode, uilock); + cycleDiagnostics(); + } + + return processHandle; +} + + +HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, + const QString &arguments, + const QString &profileName, + const QDir ¤tDirectory, + const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries) +{ + prepareStart(); + + if (!binary.exists()) { + reportError( + tr("Executable not found: %1").arg(qUtf8Printable(binary.absoluteFilePath()))); + return INVALID_HANDLE_VALUE; + } + + if (!steamAppID.isEmpty()) { + ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str()); + } else { + ::SetEnvironmentVariableW(L"SteamAPPId", + ToWString(m_Settings.getSteamAppID()).c_str()); + } + + QWidget *window = qApp->activeWindow(); + if ((window != nullptr) && (!window->isVisible())) { + window = nullptr; + } + + // This could possibly be extracted somewhere else but it's probably for when + // we have more than one provider of game registration. + if ((QFileInfo( + managedGame()->gameDirectory().absoluteFilePath("steam_api.dll")) + .exists() + || QFileInfo(managedGame()->gameDirectory().absoluteFilePath( + "steam_api64.dll")) + .exists()) + && (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) { + if (!testForSteam()) { + QDialogButtonBox::StandardButton result; + result = QuestionBoxMemory::query(window, "steamQuery", binary.fileName(), + tr("Start Steam?"), + tr("Steam is required to be running already to correctly start the game. " + "Should MO try to start steam now?"), + QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); + if (result == QDialogButtonBox::Yes) { + startSteam(window); + } else if(result == QDialogButtonBox::Cancel) { + return INVALID_HANDLE_VALUE; + } + } + } + + while (m_DirectoryUpdate) { + ::Sleep(100); + QCoreApplication::processEvents(); + } + + // need to make sure all data is saved before we start the application + if (m_CurrentProfile != nullptr) { + m_CurrentProfile->writeModlistNow(true); + } + + // TODO: should also pass arguments + if (m_AboutToRun(binary.absoluteFilePath())) { + try { + m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); + m_USVFS.updateForcedLibraries(forcedLibraries); + + } catch (const UsvfsConnectorException &e) { + qDebug(e.what()); + return INVALID_HANDLE_VALUE; + } catch (const std::exception &e) { + QMessageBox::warning(window, tr("Error"), e.what()); + return INVALID_HANDLE_VALUE; + } + + // Check if the Windows Event Logging service is running. For some reason, this seems to be + // critical to the successful running of usvfs. + if (!checkService()) { + if (QuestionBoxMemory::query(window, QString("eventLogService"), binary.fileName(), + tr("Windows Event Log Error"), + tr("The Windows Event Log service is disabled and/or not running. This prevents" + " USVFS from running properly. Your mods may not be working in the executable" + " that you are launching. Note that you may have to restart MO and/or your PC" + " after the service is fixed.\n\nContinue launching %1?").arg(binary.fileName()), + QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) { + return INVALID_HANDLE_VALUE; + } + } + + for (auto exec : settings().executablesBlacklist().split(";")) { + if (exec.compare(binary.fileName(), Qt::CaseInsensitive) == 0) { + if (QuestionBoxMemory::query(window, QString("blacklistedExecutable"), binary.fileName(), + tr("Blacklisted Executable"), + tr("The executable you are attempted to launch is blacklisted in the virtual file" + " system. This will likely prevent the executable, and any executables that are" + " launched by this one, from seeing any mods. This could extend to INI files, save" + " games and any other virtualized files.\n\nContinue launching %1?").arg(binary.fileName()), + QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) { + return INVALID_HANDLE_VALUE; + } + } + } + + QString modsPath = settings().getModDirectory(); + + // Check if this a request with either an executable or a working directory under our mods folder + // then will start the process in a virtualized "environment" with the appropriate paths fixed: + // (i.e. mods\FNIS\path\exe => game\data\path\exe) + QString cwdPath = currentDirectory.absolutePath(); + bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); + QString binPath = binary.absoluteFilePath(); + bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); + if (virtualizedCwd || virtualizedBin) { + if (virtualizedCwd) { + int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); + QString adjustedCwd = cwdPath.mid(cwdOffset, -1); + cwdPath = m_GamePlugin->dataDirectory().absolutePath(); + if (cwdOffset >= 0) + cwdPath += adjustedCwd; + + } + + if (virtualizedBin) { + int binOffset = binPath.indexOf('/', modsPath.length() + 1); + QString adjustedBin = binPath.mid(binOffset, -1); + binPath = m_GamePlugin->dataDirectory().absolutePath(); + if (binOffset >= 0) + binPath += adjustedBin; + } + + QString cmdline + = QString("launch \"%1\" \"%2\" %3") + .arg(QDir::toNativeSeparators(cwdPath), + QDir::toNativeSeparators(binPath), arguments); + + qDebug() << "Spawning proxyed process <" << cmdline << ">"; + + return startBinary(QFileInfo(QCoreApplication::applicationFilePath()), + cmdline, QCoreApplication::applicationDirPath(), true); + } else { + qDebug() << "Spawning direct process <" << binPath << "," << arguments << "," << cwdPath << ">"; + return startBinary(binary, arguments, currentDirectory, true); + } + } else { + qDebug("start of \"%s\" canceled by plugin", + qUtf8Printable(binary.absoluteFilePath())); + return INVALID_HANDLE_VALUE; + } +} + +HANDLE OrganizerCore::runShortcut(const MOShortcut& shortcut) +{ + if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance()) + throw std::runtime_error( + QString("Refusing to run executable from different instance %1:%2") + .arg(shortcut.instance(),shortcut.executable()) + .toLocal8Bit().constData()); + + Executable& exe = m_ExecutablesList.find(shortcut.executable()); + auto forcedLibaries = m_CurrentProfile->determineForcedLibraries(shortcut.executable()); + if (!m_CurrentProfile->forcedLibrariesEnabled(shortcut.executable())) { + forcedLibaries.clear(); + } + + return spawnBinaryDirect( + exe.m_BinaryInfo, exe.m_Arguments, + m_CurrentProfile->name(), + exe.m_WorkingDirectory.length() != 0 + ? exe.m_WorkingDirectory + : exe.m_BinaryInfo.absolutePath(), + exe.m_SteamAppID, + "", + forcedLibaries); +} + +HANDLE OrganizerCore::startApplication(const QString &executable, + const QStringList &args, + const QString &cwd, + const QString &profile) +{ + QFileInfo binary; + QString arguments = args.join(" "); + QString currentDirectory = cwd; + QString profileName = profile; + if (profile.length() == 0) { + if (m_CurrentProfile != nullptr) { + profileName = m_CurrentProfile->name(); + } else { + throw MyException(tr("No profile set")); + } + } + QString steamAppID; + QString customOverwrite; + QList forcedLibraries; + if (executable.contains('\\') || executable.contains('/')) { + // file path + + binary = QFileInfo(executable); + if (binary.isRelative()) { + // relative path, should be relative to game directory + binary = QFileInfo( + managedGame()->gameDirectory().absoluteFilePath(executable)); + } + if (cwd.length() == 0) { + currentDirectory = binary.absolutePath(); + } + try { + const Executable &exe = m_ExecutablesList.findByBinary(binary); + steamAppID = exe.m_SteamAppID; + customOverwrite + = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) + .toString(); + if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { + forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); + } + } catch (const std::runtime_error &) { + // nop + } + } else { + // only a file name, search executables list + try { + const Executable &exe = m_ExecutablesList.find(executable); + steamAppID = exe.m_SteamAppID; + customOverwrite + = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) + .toString(); + if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { + forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); + } + if (arguments == "") { + arguments = exe.m_Arguments; + } + binary = exe.m_BinaryInfo; + if (cwd.length() == 0) { + currentDirectory = exe.m_WorkingDirectory; + } + } catch (const std::runtime_error &) { + qWarning("\"%s\" not set up as executable", + qUtf8Printable(executable)); + binary = QFileInfo(executable); + } + } + + return spawnBinaryDirect(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); +} + +bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode) +{ + if (!Settings::instance().lockGUI()) + return true; + + ILockedWaitingForProcess* uilock = nullptr; + if (m_UserInterface != nullptr) { + uilock = m_UserInterface->lock(); + } + + ON_BLOCK_EXIT([&] () { + if (m_UserInterface != nullptr) { + m_UserInterface->unlock(); + } }); + return waitForProcessCompletion(handle, exitCode, uilock); +} + +bool OrganizerCore::waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) +{ + bool originalHandle = true; + bool newHandle = true; + bool uiunlocked = false; + + DWORD currentPID = 0; + QString processName; + auto waitForChildUntil = GetTickCount64(); + if (handle != INVALID_HANDLE_VALUE) { + currentPID = GetProcessId(handle); + processName = QString::fromStdWString(getProcessName(handle)); + } + + // Certain process names we wish to "hide" for aesthetic reason: + bool waitingOnHidden = false; + std::vector hiddenList; + hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); + for (QString hide : hiddenList) + if (processName.contains(hide, Qt::CaseInsensitive)) + waitingOnHidden = true; + // The main reason for adding the hidden list is to hide the MO proxy we use to spawn virtualized processes. + // On the one hand we want to display the real executable without it feeling laggy, on the other we don't want + // to requery processes all the time if for some reason we are waiting on hidden processes and find no "unhidden" + // process. For this reason we use exponential backoff and also start with a delibrately low value to improve + // the responsiveness of the initial update + DWORD64 nextHiddenCheck = GetTickCount64(); + DWORD64 nextHiddenCheckDelay = 50; + + constexpr DWORD INPUT_EVENT = WAIT_OBJECT_0 + 1; + DWORD res = WAIT_TIMEOUT; + while (handle != INVALID_HANDLE_VALUE && (newHandle || res == WAIT_TIMEOUT || res == INPUT_EVENT)) + { + if (newHandle) { + processName += QString(" (%1)").arg(currentPID); + if (uilock) + uilock->setProcessName(processName); + qDebug() << "Waiting for" + << (originalHandle ? "spawned" : "usvfs") + << "process completion :" << qUtf8Printable(processName); + newHandle = false; + } + + // Wait for a an event on the handle, a key press, mouse click or timeout + res = MsgWaitForMultipleObjects(1, &handle, FALSE, 200, QS_KEY | QS_MOUSEBUTTON); + if (res == WAIT_FAILED) { + qWarning() << "Failed waiting for process completion : MsgWaitForMultipleObjects WAIT_FAILED" << GetLastError(); + break; + } + + // keep processing events so the app doesn't appear dead + QCoreApplication::sendPostedEvents(); + QCoreApplication::processEvents(); + + if (uilock && uilock->unlockForced()) { + uiunlocked = true; + break; + } + + if (res == WAIT_OBJECT_0) { + // process we were waiting on has completed + if (originalHandle && exitCode && !::GetExitCodeProcess(handle, exitCode)) + qWarning() << "Failed getting exit code of complete process :" << GetLastError(); + CloseHandle(handle); + handle = INVALID_HANDLE_VALUE; + originalHandle = false; + // if the previous process spawned a child process and immediately exits we may miss it if we check immediately + waitForChildUntil = GetTickCount64() + 800; + } + + // search for another process to wait on if either: + // 1. we just completed waiting for a process and need to find/wait for an inject child + // 2. we are currently waiting on a hidden process so periodically check if there is a non-hidden process to wait on + bool firstIteration = true; + while ((handle == INVALID_HANDLE_VALUE && GetTickCount64() <= waitForChildUntil) + || (waitingOnHidden && GetTickCount64() >= nextHiddenCheck)) + { + if (firstIteration) + firstIteration = false; + else { + QThread::msleep(200); + QCoreApplication::sendPostedEvents(); + QCoreApplication::processEvents(); + } + + // search if there is another usvfs process active + handle = findAndOpenAUSVFSProcess(hiddenList, currentPID); + waitingOnHidden = false; + newHandle = handle != INVALID_HANDLE_VALUE; + if (newHandle) { + currentPID = GetProcessId(handle); + processName = QString::fromStdWString(getProcessName(handle)); + for (QString hide : hiddenList) + if (processName.contains(hide, Qt::CaseInsensitive)) + waitingOnHidden = true; + } + if (waitingOnHidden) { + nextHiddenCheck = GetTickCount64() + nextHiddenCheckDelay; + nextHiddenCheckDelay = std::min(nextHiddenCheckDelay * 2, (DWORD64) 2000); + } + else { + nextHiddenCheck = GetTickCount64(); + nextHiddenCheckDelay = 200; + } + } + } + + if (res == WAIT_OBJECT_0) + qDebug() << "Waiting for process completion successfull"; + else if (uiunlocked) + qDebug() << "Waiting for process completion aborted by UI"; + else + qDebug() << "Waiting for process completion not successfull :" << res; + + if (handle != INVALID_HANDLE_VALUE) + ::CloseHandle(handle); + + return res == WAIT_OBJECT_0; +} + +HANDLE OrganizerCore::findAndOpenAUSVFSProcess(const std::vector& hiddenList, DWORD preferedParentPid) { + // for practical reasons a querySize of 1 is probably enough, we use a larger query as a heuristics + // to find a more "aesthetic injected processes (attempting to comply to hiddenList and preferedParentPid) + constexpr size_t querySize = 100; + DWORD pids[querySize]; + size_t found = querySize; + if (!::GetVFSProcessList(&found, pids)) { + qWarning() << "Failed seeking USVFS processes : GetVFSProcessList failed?!"; + return INVALID_HANDLE_VALUE; + } + + HANDLE best_match = INVALID_HANDLE_VALUE; + bool best_match_hidden = true; + for (size_t i = 0; i < found; ++i) { + if (pids[i] == GetCurrentProcessId()) + continue; // obviously don't wait for MO process + + HANDLE handle = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pids[i]); + if (handle == INVALID_HANDLE_VALUE) { + qWarning() << "Failed openning USVFS process " << pids[i] << " : OpenProcess failed" << GetLastError(); + continue; + } + + QString pname = QString::fromStdWString(getProcessName(handle)); + bool phidden = false; + for (auto hide : hiddenList) + if (pname.contains(hide, Qt::CaseInsensitive)) + phidden = true; + + bool pprefered = preferedParentPid && getProcessParentID(pids[i]) == preferedParentPid; + + if (best_match == INVALID_HANDLE_VALUE || best_match_hidden || (!phidden && pprefered)) { + if (best_match != INVALID_HANDLE_VALUE) + CloseHandle(best_match); + best_match = handle; + best_match_hidden = phidden; + } + else + CloseHandle(handle); + + if (!phidden && pprefered) + return best_match; + } + + return best_match; +} + +bool OrganizerCore::onAboutToRun( + const std::function &func) +{ + auto conn = m_AboutToRun.connect(func); + return conn.connected(); +} + +bool OrganizerCore::onFinishedRun( + const std::function &func) +{ + auto conn = m_FinishedRun.connect(func); + return conn.connected(); +} + +bool OrganizerCore::onModInstalled( + const std::function &func) +{ + auto conn = m_ModInstalled.connect(func); + return conn.connected(); +} + +void OrganizerCore::refreshModList(bool saveChanges) +{ + // don't lose changes! + if (saveChanges) { + m_CurrentProfile->writeModlistNow(true); + } + ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, + m_PluginContainer, m_Settings.displayForeign(), managedGame()); + + m_CurrentProfile->refreshModStatus(); + + m_ModList.notifyChange(-1); + + refreshDirectoryStructure(); +} + +void OrganizerCore::refreshESPList(bool force) +{ + if (m_DirectoryUpdate) { + // don't mess up the esp list if we're currently updating the directory + // structure + m_PostRefreshTasks.append([=]() { + this->refreshESPList(force); + }); + return; + } + m_CurrentProfile->writeModlist(); + + // clear list + try { + m_PluginList.refresh(m_CurrentProfile->name(), *m_DirectoryStructure, + m_CurrentProfile->getLockedOrderFileName(), force); + } catch (const std::exception &e) { + reportError(tr("Failed to refresh list of esps: %1").arg(e.what())); + } +} + +void OrganizerCore::refreshBSAList() +{ + DataArchives *archives = m_GamePlugin->feature(); + + if (archives != nullptr) { + m_ArchivesInit = false; + + // default archives are the ones enabled outside MO. if the list can't be + // found (which might + // happen if ini files are missing) use hard-coded defaults (preferrably the + // same the game would use) + m_DefaultArchives = archives->archives(m_CurrentProfile); + if (m_DefaultArchives.length() == 0) { + m_DefaultArchives = archives->vanillaArchives(); + } + + m_ActiveArchives.clear(); + + auto iter = enabledArchives(); + m_ActiveArchives = toStringList(iter.begin(), iter.end()); + if (m_ActiveArchives.isEmpty()) { + m_ActiveArchives = m_DefaultArchives; + } + + if (m_UserInterface != nullptr) { + m_UserInterface->updateBSAList(m_DefaultArchives, m_ActiveArchives); + } + + m_ArchivesInit = true; + } +} + +void OrganizerCore::refreshLists() +{ + if ((m_CurrentProfile != nullptr) && m_DirectoryStructure->isPopulated()) { + refreshESPList(true); + refreshBSAList(); + } // no point in refreshing lists if no files have been added to the directory + // tree +} + +void OrganizerCore::updateModActiveState(int index, bool active) +{ + QList modsToUpdate; + modsToUpdate.append(index); + updateModsActiveState(modsToUpdate, active); +} + +void OrganizerCore::updateModsActiveState(const QList &modIndices, bool active) +{ + int enabled = 0; + for (auto index : modIndices) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + QDir dir(modInfo->absolutePath()); + for (const QString &esm : + dir.entryList(QStringList() << "*.esm", QDir::Files)) { + const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esm)); + if (file.get() == nullptr) { + qWarning("failed to activate %s", qUtf8Printable(esm)); + continue; + } + + if (active != m_PluginList.isEnabled(esm) + && file->getAlternatives().empty()) { + m_PluginList.blockSignals(true); + m_PluginList.enableESP(esm, active); + m_PluginList.blockSignals(false); + } + } + + for (const QString &esl : + dir.entryList(QStringList() << "*.esl", QDir::Files)) { + const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esl)); + if (file.get() == nullptr) { + qWarning("failed to activate %s", qUtf8Printable(esl)); + continue; + } + + if (active != m_PluginList.isEnabled(esl) + && file->getAlternatives().empty()) { + m_PluginList.blockSignals(true); + m_PluginList.enableESP(esl, active); + m_PluginList.blockSignals(false); + ++enabled; + } + } + QStringList esps = dir.entryList(QStringList() << "*.esp", QDir::Files); + for (const QString &esp : esps) { + const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esp)); + if (file.get() == nullptr) { + qWarning("failed to activate %s", qUtf8Printable(esp)); + continue; + } + + if (active != m_PluginList.isEnabled(esp) + && file->getAlternatives().empty()) { + m_PluginList.blockSignals(true); + m_PluginList.enableESP(esp, active); + m_PluginList.blockSignals(false); + ++enabled; + } + } + } + if (active && (enabled > 1)) { + MessageDialog::showMessage( + tr("Multiple esps/esls activated, please check that they don't conflict."), + qApp->activeWindow()); + } + m_PluginList.refreshLoadOrder(); + // immediately save affected lists + m_PluginListsWriter.writeImmediately(false); +} + +void OrganizerCore::updateModInDirectoryStructure(unsigned int index, + ModInfo::Ptr modInfo) +{ + QMap allModInfo; + allModInfo[index] = modInfo; + updateModsInDirectoryStructure(allModInfo); +} + +void OrganizerCore::updateModsInDirectoryStructure(QMap modInfo) +{ + for (auto idx : modInfo.keys()) { + // add files of the bsa to the directory structure + m_DirectoryRefresher.addModFilesToStructure( + m_DirectoryStructure, modInfo[idx]->name(), + m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(), + modInfo[idx]->stealFiles()); + } + DirectoryRefresher::cleanStructure(m_DirectoryStructure); + // need to refresh plugin list now so we can activate esps + refreshESPList(true); + // activate all esps of the specified mod so the bsas get activated along with + // it + m_PluginList.blockSignals(true); + updateModsActiveState(modInfo.keys(), true); + m_PluginList.blockSignals(false); + // now we need to refresh the bsa list and save it so there is no confusion + // about what archives are avaiable and active + refreshBSAList(); + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().writeImmediately(false); + } + + std::vector archives = enabledArchives(); + m_DirectoryRefresher.setMods( + m_CurrentProfile->getActiveMods(), + std::set(archives.begin(), archives.end())); + + // finally also add files from bsas to the directory structure + for (auto idx : modInfo.keys()) { + m_DirectoryRefresher.addModBSAToStructure( + m_DirectoryStructure, modInfo[idx]->name(), + m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(), + modInfo[idx]->archives()); + } +} + +void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply) +{ + if (m_PluginContainer != nullptr) { + for (IPluginModPage *modPage : + m_PluginContainer->plugins()) { + ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo(); + if (modPage->handlesDownload(url, reply->url(), *fileInfo)) { + fileInfo->repository = modPage->name(); + m_DownloadManager.addDownload(reply, fileInfo); + return; + } + } + } + + // no mod found that could handle the download. Is it a nexus mod? + if (url.host() == "www.nexusmods.com") { + QString gameName = ""; + int modID = 0; + int fileID = 0; + QRegExp nameExp("www\\.nexusmods\\.com/(\\a+)/"); + if (nameExp.indexIn(url.toString()) != -1) { + gameName = nameExp.cap(1); + } + QRegExp modExp("mods/(\\d+)"); + if (modExp.indexIn(url.toString()) != -1) { + modID = modExp.cap(1).toInt(); + } + QRegExp fileExp("fid=(\\d+)"); + if (fileExp.indexIn(reply->url().toString()) != -1) { + fileID = fileExp.cap(1).toInt(); + } + m_DownloadManager.addDownload(reply, + new ModRepositoryFileInfo(gameName, modID, fileID)); + } else { + if (QMessageBox::question(qApp->activeWindow(), tr("Download?"), + tr("A download has been started but no installed " + "page plugin recognizes it.\n" + "If you download anyway no information (i.e. " + "version) will be associated with the " + "download.\n" + "Continue?"), + QMessageBox::Yes | QMessageBox::No) + == QMessageBox::Yes) { + m_DownloadManager.addDownload(reply, new ModRepositoryFileInfo()); + } + } +} + +ModListSortProxy *OrganizerCore::createModListProxyModel() +{ + ModListSortProxy *result = new ModListSortProxy(m_CurrentProfile, this); + result->setSourceModel(&m_ModList); + return result; +} + +PluginListSortProxy *OrganizerCore::createPluginListProxyModel() +{ + PluginListSortProxy *result = new PluginListSortProxy(this); + result->setSourceModel(&m_PluginList); + return result; +} + +IPluginGame const *OrganizerCore::managedGame() const +{ + return m_GamePlugin; +} + +std::vector OrganizerCore::enabledArchives() +{ + std::vector result; + if (m_ArchiveParsing) { + QFile archiveFile(m_CurrentProfile->getArchivesFileName()); + if (archiveFile.open(QIODevice::ReadOnly)) { + while (!archiveFile.atEnd()) { + result.push_back(QString::fromUtf8(archiveFile.readLine()).trimmed()); + } + archiveFile.close(); + } + } + return result; +} + +void OrganizerCore::refreshDirectoryStructure() +{ + if (!m_DirectoryUpdate) { + m_CurrentProfile->writeModlistNow(true); + + m_DirectoryUpdate = true; + std::vector> activeModList + = m_CurrentProfile->getActiveMods(); + auto archives = enabledArchives(); + m_DirectoryRefresher.setMods( + activeModList, std::set(archives.begin(), archives.end())); + + QTimer::singleShot(0, &m_DirectoryRefresher, SLOT(refresh())); + } +} + +void OrganizerCore::directory_refreshed() +{ + DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure(); + Q_ASSERT(newStructure != m_DirectoryStructure); + if (newStructure != nullptr) { + std::swap(m_DirectoryStructure, newStructure); + delete newStructure; + } else { + // TODO: don't know why this happens, this slot seems to get called twice + // with only one emit + return; + } + m_DirectoryUpdate = false; + + for (int i = 0; i < m_ModList.rowCount(); ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + modInfo->clearCaches(); + } + for (auto task : m_PostRefreshTasks) { + task(); + } + m_PostRefreshTasks.clear(); + + if (m_CurrentProfile != nullptr) { + refreshLists(); + } +} + +void OrganizerCore::profileRefresh() +{ + // have to refresh mods twice (again in refreshModList), otherwise the refresh + // isn't complete. Not sure why + ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, + m_PluginContainer, m_Settings.displayForeign(), managedGame()); + m_CurrentProfile->refreshModStatus(); + + refreshModList(); +} + +void OrganizerCore::modStatusChanged(unsigned int index) +{ + try { + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + if (m_CurrentProfile->modEnabled(index)) { + updateModInDirectoryStructure(index, modInfo); + } else { + updateModActiveState(index, false); + if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { + FilesOrigin &origin + = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + } + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().write(); + } + } + modInfo->clearCaches(); + + for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + int priority = m_CurrentProfile->getModPriority(i); + if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { + // priorities in the directory structure are one higher because data is + // 0 + m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())) + .setPriority(priority + 1); + } + } + m_DirectoryStructure->getFileRegister()->sortOrigins(); + + refreshLists(); + } catch (const std::exception &e) { + reportError(tr("failed to update mod list: %1").arg(e.what())); + } +} + +void OrganizerCore::modStatusChanged(QList index) { + try { + QMap modsToEnable; + QMap modsToDisable; + for (auto idx : index) { + if (m_CurrentProfile->modEnabled(idx)) { + modsToEnable[idx] = ModInfo::getByIndex(idx); + } else { + modsToDisable[idx] = ModInfo::getByIndex(idx); + } + } + if (!modsToEnable.isEmpty()) { + updateModsInDirectoryStructure(modsToEnable); + for (auto modInfo : modsToEnable.values()) { + modInfo->clearCaches(); + } + } + if (!modsToDisable.isEmpty()) { + updateModsActiveState(modsToDisable.keys(), false); + for (auto idx : modsToDisable.keys()) { + if (m_DirectoryStructure->originExists(ToWString(modsToDisable[idx]->name()))) { + FilesOrigin &origin + = m_DirectoryStructure->getOriginByName(ToWString(modsToDisable[idx]->name())); + origin.enable(false); + } + } + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().write(); + } + } + + for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + int priority = m_CurrentProfile->getModPriority(i); + if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { + // priorities in the directory structure are one higher because data is + // 0 + m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())) + .setPriority(priority + 1); + } + } + m_DirectoryStructure->getFileRegister()->sortOrigins(); + + refreshLists(); + } catch (const std::exception &e) { + reportError(tr("failed to update mod list: %1").arg(e.what())); + } +} + +void OrganizerCore::loginSuccessful(bool necessary) +{ + if (necessary) { + MessageDialog::showMessage(tr("login successful"), qApp->activeWindow()); + } + for (QString url : m_PendingDownloads) { + downloadRequestedNXM(url); + } + m_PendingDownloads.clear(); + for (auto task : m_PostLoginTasks) { + task(); + } + + m_PostLoginTasks.clear(); + NexusInterface::instance(m_PluginContainer)->loginCompleted(); +} + +void OrganizerCore::loginSuccessfulUpdate(bool necessary) +{ + if (necessary) { + MessageDialog::showMessage(tr("login successful"), qApp->activeWindow()); + } + m_Updater.startUpdate(); +} + +void OrganizerCore::loginFailed(const QString &message) +{ + if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"), + tr("Login failed, try again?")) + == QMessageBox::Yes) { + if (nexusApi(true)) { + return; + } + } + + if (!m_PendingDownloads.isEmpty()) { + MessageDialog::showMessage( + tr("login failed: %1. Download will not be associated with an account") + .arg(message), + qApp->activeWindow()); + for (QString url : m_PendingDownloads) { + downloadRequestedNXM(url); + } + m_PendingDownloads.clear(); + } else { + MessageDialog::showMessage(tr("login failed: %1").arg(message), + qApp->activeWindow()); + m_PostLoginTasks.clear(); + } + NexusInterface::instance(m_PluginContainer)->loginCompleted(); +} + +void OrganizerCore::loginFailedUpdate(const QString &message) +{ + MessageDialog::showMessage( + tr("login failed: %1. You need to log-in with Nexus to update MO.") + .arg(message), + qApp->activeWindow()); +} + +void OrganizerCore::syncOverwrite() +{ + unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { + std::vector flags = mod->getFlags(); + return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) + != flags.end(); + }); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); + SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure, + qApp->activeWindow()); + if (syncDialog.exec() == QDialog::Accepted) { + syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory())); + modInfo->testValid(); + refreshDirectoryStructure(); + } +} + +QString OrganizerCore::oldMO1HookDll() const +{ + if (auto extender = managedGame()->feature()) { + QString hookdll = QDir::toNativeSeparators( + managedGame()->dataDirectory().absoluteFilePath(extender->PluginPath() + "/hook.dll")); + if (QFile(hookdll).exists()) + return hookdll; + } + return QString(); +} + +std::vector OrganizerCore::activeProblems() const +{ + std::vector problems; + const auto& hookdll = oldMO1HookDll(); + if (!hookdll.isEmpty()) { + // This warning will now be shown every time the problems are checked, which is a bit + // of a "log spam". But since this is a sevre error which will most likely make the + // game crash/freeze/etc. and is very hard to diagnose, this "log spam" will make it + // easier for the user to notice the warning. + qWarning("hook.dll found in game folder: %s", qUtf8Printable(hookdll)); + problems.push_back(PROBLEM_MO1SCRIPTEXTENDERWORKAROUND); + } + return problems; +} + +QString OrganizerCore::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: { + return tr("MO1 \"Script Extender\" load mechanism has left hook.dll in your game folder"); + } break; + default: { + return tr("Description missing"); + } break; + } +} + +QString OrganizerCore::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: { + return tr("hook.dll has been found in your game folder (right click to copy the full path). " + "This is most likely a leftover of setting the ModOrganizer 1 load mechanism to \"Script Extender\", " + "in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or " + "manually removing the file, otherwise the game is likely to crash and burn.").arg(oldMO1HookDll()); + break; + } + default: { + return tr("Description missing"); + } break; + } +} + +bool OrganizerCore::hasGuidedFix(unsigned int) const +{ + return false; +} + +void OrganizerCore::startGuidedFix(unsigned int) const +{ +} + +bool OrganizerCore::saveCurrentLists() +{ + if (m_DirectoryUpdate) { + qWarning("not saving lists during directory update"); + return false; + } + + try { + savePluginList(); + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().write(); + } + } catch (const std::exception &e) { + reportError(tr("failed to save load order: %1").arg(e.what())); + } + + return true; +} + +void OrganizerCore::savePluginList() +{ + if (m_DirectoryUpdate) { + // delay save till after directory update + m_PostRefreshTasks.append([this]() { + this->savePluginList(); + }); + return; + } + m_PluginList.saveTo(m_CurrentProfile->getLockedOrderFileName(), + m_CurrentProfile->getDeleterFileName(), + m_Settings.hideUncheckedPlugins()); + m_PluginList.saveLoadOrder(*m_DirectoryStructure); +} + +void OrganizerCore::prepareStart() +{ + if (m_CurrentProfile == nullptr) { + return; + } + m_CurrentProfile->writeModlist(); + m_CurrentProfile->createTweakedIniFile(); + saveCurrentLists(); + m_Settings.setupLoadMechanism(); + storeSettings(); +} + +std::vector OrganizerCore::fileMapping(const QString &profileName, + const QString &customOverwrite) +{ + // need to wait until directory structure + while (m_DirectoryUpdate) { + ::Sleep(100); + QCoreApplication::processEvents(); + } + + IPluginGame *game = qApp->property("managed_game").value(); + Profile profile(QDir(m_Settings.getProfileDirectory() + "/" + profileName), + game); + + MappingType result; + + QString dataPath + = QDir::toNativeSeparators(game->dataDirectory().absolutePath()); + + bool overwriteActive = false; + + for (auto mod : profile.getActiveMods()) { + if (std::get<0>(mod).compare("overwrite", Qt::CaseInsensitive) == 0) { + continue; + } + + unsigned int modIndex = ModInfo::getIndex(std::get<0>(mod)); + ModInfo::Ptr modPtr = ModInfo::getByIndex(modIndex); + + bool createTarget = customOverwrite == std::get<0>(mod); + + overwriteActive |= createTarget; + + if (modPtr->isRegular()) { + result.insert(result.end(), {QDir::toNativeSeparators(std::get<1>(mod)), + dataPath, true, createTarget}); + } + } + + if (!overwriteActive && !customOverwrite.isEmpty()) { + throw MyException(tr("The designated write target \"%1\" is not enabled.") + .arg(customOverwrite)); + } + + if (m_CurrentProfile->localSavesEnabled()) { + LocalSavegames *localSaves = game->feature(); + if (localSaves != nullptr) { + MappingType saveMap + = localSaves->mappings(currentProfile()->absolutePath() + "/saves"); + result.reserve(result.size() + saveMap.size()); + result.insert(result.end(), saveMap.begin(), saveMap.end()); + } else { + qWarning("local save games not supported by this game plugin"); + } + } + + result.insert(result.end(), { + QDir::toNativeSeparators(m_Settings.getOverwriteDirectory()), + dataPath, + true, + customOverwrite.isEmpty() + }); + + for (MOBase::IPluginFileMapper *mapper : + m_PluginContainer->plugins()) { + IPlugin *plugin = dynamic_cast(mapper); + if (plugin->isActive()) { + MappingType pluginMap = mapper->mappings(); + result.reserve(result.size() + pluginMap.size()); + result.insert(result.end(), pluginMap.begin(), pluginMap.end()); + } + } + + return result; +} + + +std::vector OrganizerCore::fileMapping( + const QString &dataPath, const QString &relPath, const DirectoryEntry *base, + const DirectoryEntry *directoryEntry, int createDestination) +{ + std::vector result; + + for (FileEntry::Ptr current : directoryEntry->getFiles()) { + bool isArchive = false; + int origin = current->getOrigin(isArchive); + if (isArchive || (origin == 0)) { + continue; + } + + QString originPath + = QString::fromStdWString(base->getOriginByID(origin).getPath()); + QString fileName = QString::fromStdWString(current->getName()); +// QString fileName = ToQString(current->getName()); + QString source = originPath + relPath + fileName; + QString target = dataPath + relPath + fileName; + if (source != target) { + result.push_back({source, target, false, false}); + } + } + + // recurse into subdirectories + std::vector::const_iterator current, end; + directoryEntry->getSubDirectories(current, end); + for (; current != end; ++current) { + int origin = (*current)->anyOrigin(); + + QString originPath + = QString::fromStdWString(base->getOriginByID(origin).getPath()); + QString dirName = QString::fromStdWString((*current)->getName()); + QString source = originPath + relPath + dirName; + QString target = dataPath + relPath + dirName; + + bool writeDestination + = (base == directoryEntry) && (origin == createDestination); + + result.push_back({source, target, true, writeDestination}); + std::vector subRes = fileMapping( + dataPath, relPath + dirName + "\\", base, *current, createDestination); + result.insert(result.end(), subRes.begin(), subRes.end()); + } + return result; +} diff --git a/src/organizercore.h b/src/organizercore.h index 68d4e7e4..0a4cff6c 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -245,7 +245,7 @@ public slots: void requestDownload(const QUrl &url, QNetworkReply *reply); void downloadRequestedNXM(const QString &url); - bool nexusLogin(bool retry = false); + bool nexusApi(bool retry = false); signals: @@ -267,7 +267,7 @@ private: QString commitSettings(const QString &iniFile); - bool queryLogin(QString &username, QString &password); + bool queryApi(QString &apiKey); void updateModActiveState(int index, bool active); void updateModsActiveState(const QList &modIndices, bool active); @@ -341,7 +341,6 @@ private: QThread m_RefresherThread; - bool m_AskForNexusPW; bool m_DirectoryUpdate; bool m_ArchivesInit; bool m_ArchiveParsing{ m_Settings.archiveParsing() }; diff --git a/src/settings.cpp b/src/settings.cpp index 0ce647fd..7a1d6737 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -332,15 +332,12 @@ QString Settings::getNMMVersion() const return result; } -bool Settings::getNexusLogin(QString &username, QString &password) const +bool Settings::getNexusApiKey(QString &apiKey) const { - if (m_Settings.value("Settings/nexus_login", false).toBool()) { - username = m_Settings.value("Settings/nexus_username", "").toString(); - password = deObfuscate(m_Settings.value("Settings/nexus_password", "").toString()); - return true; - } else { + if (m_Settings.value("Settings/nexus_api_key", "").toString().isEmpty()) return false; - } + apiKey = deObfuscate(m_Settings.value("Settings/nexus_api_key", "").toString()); + return true; } bool Settings::getSteamLogin(QString &username, QString &password) const @@ -431,11 +428,9 @@ QString Settings::executablesBlacklist() const ).toString(); } -void Settings::setNexusLogin(QString username, QString password) +void Settings::setNexusApiKey(QString apiKey) { - m_Settings.setValue("Settings/nexus_login", true); - m_Settings.setValue("Settings/nexus_username", username); - m_Settings.setValue("Settings/nexus_password", obfuscate(password)); + m_Settings.setValue("Settings/nexus_api_key", obfuscate(apiKey)); } void Settings::setSteamLogin(QString username, QString password) @@ -674,11 +669,29 @@ void Settings::resetDialogs() QuestionBoxMemory::resetDialogs(); } +void Settings::processApiKey(const QString &apiKey) +{ + m_Settings.setValue("Settings/nexus_api_key", obfuscate(apiKey)); +} + +void Settings::checkApiKey(QPushButton *nexusButton) +{ + if (m_Settings.value("Settings/nexus_api_key", "").toString().isEmpty()) { + nexusButton->setEnabled(true); + nexusButton->setText("Connect to Nexus"); + QMessageBox::warning(qApp->activeWindow(), tr("Error"), + tr("Failed to retrieve a Nexus API key! Please try again." + "A browser window should open asking you to authorize.")); + } +} + void Settings::query(PluginContainer *pluginContainer, QWidget *parent) { SettingsDialog dialog(pluginContainer, parent); connect(&dialog, SIGNAL(resetDialogs()), this, SLOT(resetDialogs())); + connect(&dialog, SIGNAL(processApiKey(const QString &)), this, SLOT(processApiKey(const QString &))); + connect(&dialog, SIGNAL(closeApiConnection(QPushButton *)), this, SLOT(checkApiKey(QPushButton *))); std::vector> tabs; @@ -960,9 +973,7 @@ void Settings::DiagnosticsTab::update() Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) : Settings::SettingsTab(parent, dialog) - , m_loginCheckBox(dialog.findChild("loginCheckBox")) - , m_usernameEdit(dialog.findChild("usernameEdit")) - , m_passwordEdit(dialog.findChild("passwordEdit")) + , m_nexusConnect(dialog.findChild("nexusConnect")) , m_offlineBox(dialog.findChild("offlineBox")) , m_proxyBox(dialog.findChild("proxyBox")) , m_knownServersList(dialog.findChild("knownServersList")) @@ -970,10 +981,9 @@ Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) dialog.findChild("preferredServersList")) , m_endorsementBox(dialog.findChild("endorsementBox")) { - if (parent->automaticLoginEnabled()) { - m_loginCheckBox->setChecked(true); - m_usernameEdit->setText(m_Settings.value("Settings/nexus_username", "").toString()); - m_passwordEdit->setText(deObfuscate(m_Settings.value("Settings/nexus_password", "").toString())); + if (!m_Settings.value("Settings/nexus_api_key", "").toString().isEmpty()) { + m_nexusConnect->setText("Nexus API Key Stored"); + m_nexusConnect->setDisabled(true); } m_offlineBox->setChecked(parent->offlineMode()); @@ -1009,6 +1019,7 @@ Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) void Settings::NexusTab::update() { + /* if (m_loginCheckBox->isChecked()) { m_Settings.setValue("Settings/nexus_login", true); m_Settings.setValue("Settings/nexus_username", m_usernameEdit->text()); @@ -1018,6 +1029,7 @@ void Settings::NexusTab::update() m_Settings.remove("Settings/nexus_username"); m_Settings.remove("Settings/nexus_password"); } + */ m_Settings.setValue("Settings/offline_mode", m_offlineBox->isChecked()); m_Settings.setValue("Settings/use_proxy", m_proxyBox->isChecked()); m_Settings.setValue("Settings/endorsement_integration", m_endorsementBox->isChecked()); diff --git a/src/settings.h b/src/settings.h index 0286d4db..49e5a5b6 100644 --- a/src/settings.h +++ b/src/settings.h @@ -25,6 +25,7 @@ along with Mod Organizer. If not, see . #include #include #include +#include #include #include #include @@ -188,7 +189,7 @@ public: * @param password (out) received the password for nexus * @return true if automatic login is active, false otherwise **/ - bool getNexusLogin(QString &username, QString &password) const; + bool getNexusApiKey(QString &apiKey) const; /** * @brief retrieve the login information for steam @@ -249,7 +250,7 @@ public: * @param username username * @param password password */ - void setNexusLogin(QString username, QString password); + void setNexusApiKey(QString apiKey); /** * @brief set the steam login information @@ -475,13 +476,10 @@ private: { public: NexusTab(Settings *m_parent, SettingsDialog &m_dialog); - void update(); private: - QCheckBox *m_loginCheckBox; - QLineEdit *m_usernameEdit; - QLineEdit *m_passwordEdit; + QPushButton *m_nexusConnect; QCheckBox *m_offlineBox; QCheckBox *m_proxyBox; QListWidget *m_knownServersList; @@ -537,6 +535,8 @@ private: private slots: void resetDialogs(); + void processApiKey(const QString &); + void checkApiKey(QPushButton *nexusButton); signals: diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 1bbc256f..4843dea5 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -29,12 +29,17 @@ along with Mod Organizer. If not, see . #include "nexusinterface.h" #include "plugincontainer.h" +#include +#include + #include #include #include #include #include #include +#include +#include #define WIN32_LEAN_AND_MEAN #include @@ -46,6 +51,7 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, QWidget *parent : TutorableDialog("SettingsDialog", parent) , ui(new Ui::SettingsDialog) , m_PluginContainer(pluginContainer) + , m_nexusLogin(new QWebSocket) { ui->setupUi(this); ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}"); @@ -53,6 +59,9 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, QWidget *parent QShortcut *delShortcut = new QShortcut(QKeySequence(Qt::Key_Delete), ui->pluginBlacklist); connect(delShortcut, SIGNAL(activated()), this, SLOT(deleteBlacklistItem())); + connect(m_nexusLogin, SIGNAL(connected()), this, SLOT(dispatchLogin())); + connect(m_nexusLogin, SIGNAL(textMessageReceived(const QString &)), this, SLOT(receiveApiKey(const QString &))); + connect(m_nexusLogin, SIGNAL(disconnected()), this, SLOT(completeApiConnection())); } SettingsDialog::~SettingsDialog() @@ -326,6 +335,39 @@ void SettingsDialog::on_resetDialogsButton_clicked() } } +void SettingsDialog::on_nexusConnect_clicked() +{ + ui->nexusConnect->setText("Connecting the API..."); + ui->nexusConnect->setDisabled(true); + QUrl url = QUrl("wss://sso.nexusmods.com:8443"); + m_nexusLogin->open(url); +} + +void SettingsDialog::dispatchLogin() +{ + QJsonObject login; + boost::uuids::random_generator generator; + boost::uuids::uuid sessionId = generator(); + login.insert(QString("id"), QJsonValue(QString(boost::uuids::to_string(sessionId).c_str()))); + login.insert(QString("appid"), QJsonValue(QString("MO2"))); + QJsonDocument loginDoc(login); + QString finalMessage(loginDoc.toJson()); + m_nexusLogin->sendTextMessage(finalMessage); + QDesktopServices::openUrl(QUrl(QString("https://www.nexusmods.com/sso?id=") + QString(boost::uuids::to_string(sessionId).c_str()))); +} + +void SettingsDialog::receiveApiKey(const QString &apiKey) +{ + emit processApiKey(apiKey); + m_nexusLogin->close(); + ui->nexusConnect->setText("Nexus API Key Stored"); +} + +void SettingsDialog::completeApiConnection() +{ + emit closeApiConnection(ui->nexusConnect); +} + void SettingsDialog::storeSettings(QListWidgetItem *pluginItem) { if (pluginItem != nullptr) { diff --git a/src/settingsdialog.h b/src/settingsdialog.h index 6357f064..90b60c91 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -1,163 +1,177 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef SETTINGSDIALOG_H -#define SETTINGSDIALOG_H - -#include "tutorabledialog.h" -#include -#include -#include - -class PluginContainer; - -namespace Ui { - class SettingsDialog; -} - -/** - * dialog used to change settings for Mod Organizer. On top of the - * settings managed by the "Settings" class, this offers a button to open the - * CategoriesDialog - **/ -class SettingsDialog : public MOBase::TutorableDialog -{ - Q_OBJECT - -public: - explicit SettingsDialog(PluginContainer *pluginContainer, QWidget *parent = 0); - ~SettingsDialog(); - - /** - * @brief get stylesheet of settings buttons with colored background - * @return string of stylesheet - */ - QString getColoredButtonStyleSheet() const; - - void setButtonColor(QPushButton *button, QColor &color); - -public slots: - - virtual void accept(); - -signals: - - void resetDialogs(); - -private: - - void storeSettings(QListWidgetItem *pluginItem); - void normalizePath(QLineEdit *lineEdit); - -public: - - QColor getOverwritingColor() { return m_OverwritingColor; } - QColor getOverwrittenColor() { return m_OverwrittenColor; } - QColor getOverwritingArchiveColor() { return m_OverwritingArchiveColor; } - QColor getOverwrittenArchiveColor() { return m_OverwrittenArchiveColor; } - QColor getContainsColor() { return m_ContainsColor; } - QColor getContainedColor() { return m_ContainedColor; } - QString getExecutableBlacklist() { return m_ExecutableBlacklist; } - bool getResetGeometries(); - - void setOverwritingColor(QColor col) { m_OverwritingColor = col; } - void setOverwrittenColor(QColor col) { m_OverwrittenColor = col; } - void setOverwritingArchiveColor(QColor col) { m_OverwritingArchiveColor = col; } - void setOverwrittenArchiveColor(QColor col) { m_OverwrittenArchiveColor = col; } - void setContainsColor(QColor col) { m_ContainsColor = col; } - void setContainedColor(QColor col) { m_ContainedColor = col; } - void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; } - - -private slots: - void on_loginCheckBox_toggled(bool checked); - - void on_categoriesBtn_clicked(); - - void on_execBlacklistBtn_clicked(); - - void on_bsaDateBtn_clicked(); - - void on_browseDownloadDirBtn_clicked(); - - void on_browseModDirBtn_clicked(); - - void on_browseCacheDirBtn_clicked(); - - void on_resetDialogsButton_clicked(); - - void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); - - void deleteBlacklistItem(); - - void on_associateButton_clicked(); - - void on_clearCacheButton_clicked(); - - void on_browseBaseDirBtn_clicked(); - - void on_browseOverwriteDirBtn_clicked(); - - void on_browseProfilesDirBtn_clicked(); - - void on_browseGameDirBtn_clicked(); - - void on_overwritingBtn_clicked(); - - void on_overwrittenBtn_clicked(); - - void on_overwritingArchiveBtn_clicked(); - - void on_overwrittenArchiveBtn_clicked(); - - void on_containsBtn_clicked(); - - void on_containedBtn_clicked(); - - void on_resetColorsBtn_clicked(); - - void on_baseDirEdit_editingFinished(); - - void on_downloadDirEdit_editingFinished(); - - void on_modDirEdit_editingFinished(); - - void on_cacheDirEdit_editingFinished(); - - void on_profilesDirEdit_editingFinished(); - - void on_overwriteDirEdit_editingFinished(); - -private: - Ui::SettingsDialog *ui; - PluginContainer *m_PluginContainer; - - QColor m_OverwritingColor; - QColor m_OverwrittenColor; - QColor m_OverwritingArchiveColor; - QColor m_OverwrittenArchiveColor; - QColor m_ContainsColor; - QColor m_ContainedColor; - - QString m_ExecutableBlacklist; -}; - - - -#endif // SETTINGSDIALOG_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef SETTINGSDIALOG_H +#define SETTINGSDIALOG_H + +#include "tutorabledialog.h" +#include +#include +#include +#include +#include + +class PluginContainer; + +namespace Ui { + class SettingsDialog; +} + +/** + * dialog used to change settings for Mod Organizer. On top of the + * settings managed by the "Settings" class, this offers a button to open the + * CategoriesDialog + **/ +class SettingsDialog : public MOBase::TutorableDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(PluginContainer *pluginContainer, QWidget *parent = 0); + ~SettingsDialog(); + + /** + * @brief get stylesheet of settings buttons with colored background + * @return string of stylesheet + */ + QString getColoredButtonStyleSheet() const; + + void setButtonColor(QPushButton *button, QColor &color); + +public slots: + + virtual void accept(); + +signals: + + void resetDialogs(); + void processApiKey(const QString &); + void closeApiConnection(QPushButton *); + +private: + + void storeSettings(QListWidgetItem *pluginItem); + void normalizePath(QLineEdit *lineEdit); + +public: + + QColor getOverwritingColor() { return m_OverwritingColor; } + QColor getOverwrittenColor() { return m_OverwrittenColor; } + QColor getOverwritingArchiveColor() { return m_OverwritingArchiveColor; } + QColor getOverwrittenArchiveColor() { return m_OverwrittenArchiveColor; } + QColor getContainsColor() { return m_ContainsColor; } + QColor getContainedColor() { return m_ContainedColor; } + QString getExecutableBlacklist() { return m_ExecutableBlacklist; } + bool getResetGeometries(); + + void setOverwritingColor(QColor col) { m_OverwritingColor = col; } + void setOverwrittenColor(QColor col) { m_OverwrittenColor = col; } + void setOverwritingArchiveColor(QColor col) { m_OverwritingArchiveColor = col; } + void setOverwrittenArchiveColor(QColor col) { m_OverwrittenArchiveColor = col; } + void setContainsColor(QColor col) { m_ContainsColor = col; } + void setContainedColor(QColor col) { m_ContainedColor = col; } + void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; } + + +private slots: + void on_loginCheckBox_toggled(bool checked); + + void on_categoriesBtn_clicked(); + + void on_execBlacklistBtn_clicked(); + + void on_bsaDateBtn_clicked(); + + void on_browseDownloadDirBtn_clicked(); + + void on_browseModDirBtn_clicked(); + + void on_browseCacheDirBtn_clicked(); + + void on_resetDialogsButton_clicked(); + + void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + + void deleteBlacklistItem(); + + void on_associateButton_clicked(); + + void on_clearCacheButton_clicked(); + + void on_browseBaseDirBtn_clicked(); + + void on_browseOverwriteDirBtn_clicked(); + + void on_browseProfilesDirBtn_clicked(); + + void on_browseGameDirBtn_clicked(); + + void on_overwritingBtn_clicked(); + + void on_overwrittenBtn_clicked(); + + void on_overwritingArchiveBtn_clicked(); + + void on_overwrittenArchiveBtn_clicked(); + + void on_containsBtn_clicked(); + + void on_containedBtn_clicked(); + + void on_resetColorsBtn_clicked(); + + void on_baseDirEdit_editingFinished(); + + void on_downloadDirEdit_editingFinished(); + + void on_modDirEdit_editingFinished(); + + void on_cacheDirEdit_editingFinished(); + + void on_profilesDirEdit_editingFinished(); + + void on_overwriteDirEdit_editingFinished(); + + void on_nexusConnect_clicked(); + + void dispatchLogin(); + + void receiveApiKey(const QString &apiKey); + + void completeApiConnection(); + +private: + Ui::SettingsDialog *ui; + PluginContainer *m_PluginContainer; + + QColor m_OverwritingColor; + QColor m_OverwrittenColor; + QColor m_OverwritingArchiveColor; + QColor m_OverwrittenArchiveColor; + QColor m_ContainsColor; + QColor m_ContainedColor; + + QString m_ExecutableBlacklist; + + QWebSocket *m_nexusLogin; +}; + + + +#endif // SETTINGSDIALOG_H diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index c865744e..d3628ab3 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -468,49 +468,12 @@ p, li { white-space: pre-wrap; } Nexus - - - - - - - If checked and if correct credentials are entered below, log-in to Nexus (for browsing and downloading) is automatic. - - - Automatically Log-In to Nexus - - - - + - Username - - - - - - - false - - - - - - - Password - - - - - - - false - - - QLineEdit::Password + Connect to Nexus @@ -1387,9 +1350,6 @@ programs you are intentionally running. browseProfilesDirBtn overwriteDirEdit browseOverwriteDirBtn - loginCheckBox - usernameEdit - passwordEdit clearCacheButton associateButton knownServersList -- cgit v1.3.1 From 4bea346b26f2e34120a42fdf2f1c5485ab93f5c9 Mon Sep 17 00:00:00 2001 From: Silarn Date: Sun, 27 Jan 2019 17:08:42 -0600 Subject: Reworking update checks to use the file update info with a fallback --- src/downloadmanager.cpp | 3673 +++++++++++++++++++++++------------------------ src/downloadmanager.h | 1132 ++++++++------- src/mainwindow.cpp | 147 +- src/mainwindow.h | 5 +- src/modinfo.cpp | 43 +- src/modinfobackup.h | 1 + src/modinfodialog.cpp | 3154 ++++++++++++++++++++-------------------- src/modinfoforeign.h | 1 + src/modinfooverwrite.h | 1 + src/modinforegular.cpp | 14 +- src/modinforegular.h | 2 +- src/modinfoseparator.h | 18 +- src/nexusinterface.cpp | 1444 +++++++++---------- src/nexusinterface.h | 847 ++++++----- src/organizer_en.ts | 426 +++--- 15 files changed, 5455 insertions(+), 5453 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index c927695e..8f36b9cf 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -1,1842 +1,1831 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "downloadmanager.h" - -#include "nxmurl.h" -#include "nexusinterface.h" -#include "nxmaccessmanager.h" -#include "iplugingame.h" -#include "downloadmanager.h" -#include -#include -#include "utility.h" -#include "selectiondialog.h" -#include "bbcode.h" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - - -using namespace MOBase; - - -// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads - - -static const char UNFINISHED[] = ".unfinished"; - -unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U; -int DownloadManager::m_DirWatcherDisabler = 0; - - -DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs) -{ - DownloadInfo *info = new DownloadInfo; - info->m_DownloadID = s_NextDownloadID++; - info->m_StartTime.start(); - info->m_PreResumeSize = 0LL; - info->m_Progress = std::make_pair(0, "0.0 B/s "); - info->m_ResumePos = 0; - info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo); - info->m_Urls = URLs; - info->m_CurrentUrl = 0; - info->m_Tries = AUTOMATIC_RETRIES; - info->m_State = STATE_STARTED; - info->m_TaskProgressId = TaskProgressManager::instance().getId(); - info->m_Reply = nullptr; - - return info; -} - -DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory) -{ - DownloadInfo *info = new DownloadInfo; - - QString metaFileName = filePath + ".meta"; - QFileInfo metaFileInfo(metaFileName); - if (QDir::fromNativeSeparators(metaFileInfo.path()).compare(QDir::fromNativeSeparators(outputDirectory), Qt::CaseInsensitive) != 0) return nullptr; - QSettings metaFile(metaFileName, QSettings::IniFormat); - if (!showHidden && metaFile.value("removed", false).toBool()) { - return nullptr; - } else { - info->m_Hidden = metaFile.value("removed", false).toBool(); - } - - QString fileName = QFileInfo(filePath).fileName(); - - if (fileName.endsWith(UNFINISHED)) { - info->m_FileName = fileName.mid( - 0, fileName.length() - static_cast(strlen(UNFINISHED))); - info->m_State = STATE_PAUSED; - } else { - info->m_FileName = fileName; - - if (metaFile.value("paused", false).toBool()) { - info->m_State = STATE_PAUSED; - } else if (metaFile.value("uninstalled", false).toBool()) { - info->m_State = STATE_UNINSTALLED; - } else if (metaFile.value("installed", false).toBool()) { - info->m_State = STATE_INSTALLED; - } else { - info->m_State = STATE_READY; - } - } - - info->m_DownloadID = s_NextDownloadID++; - info->m_Output.setFileName(filePath); - info->m_TotalSize = QFileInfo(filePath).size(); - info->m_PreResumeSize = info->m_TotalSize; - info->m_CurrentUrl = 0; - info->m_Urls = metaFile.value("url", "").toString().split(";"); - info->m_Tries = 0; - info->m_TaskProgressId = TaskProgressManager::instance().getId(); - QString gameName = metaFile.value("gameName", "").toString(); - int modID = metaFile.value("modID", 0).toInt(); - int fileID = metaFile.value("fileID", 0).toInt(); - info->m_FileInfo = new ModRepositoryFileInfo(gameName, modID, fileID); - info->m_FileInfo->name = metaFile.value("name", "").toString(); - if (info->m_FileInfo->name == "0") { - // bug in earlier version - info->m_FileInfo->name = ""; - } - info->m_FileInfo->modName = metaFile.value("modName", "").toString(); - info->m_FileInfo->gameName = gameName; - info->m_FileInfo->modID = modID; - info->m_FileInfo->fileID = fileID; - info->m_FileInfo->description = metaFile.value("description").toString(); - info->m_FileInfo->version.parse(metaFile.value("version", "0").toString()); - info->m_FileInfo->newestVersion.parse(metaFile.value("newestVersion", "0").toString()); - info->m_FileInfo->categoryID = metaFile.value("category", 0).toInt(); - info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt(); - info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString(); - info->m_FileInfo->userData = metaFile.value("userData").toMap(); - info->m_Reply = nullptr; - - return info; -} - -void DownloadManager::startDisableDirWatcher() -{ - DownloadManager::m_DirWatcherDisabler++; -} - - -void DownloadManager::endDisableDirWatcher() -{ - if (DownloadManager::m_DirWatcherDisabler > 0) - { - if (DownloadManager::m_DirWatcherDisabler == 1) - QCoreApplication::processEvents(); - DownloadManager::m_DirWatcherDisabler--; - } - else { - DownloadManager::m_DirWatcherDisabler = 0; - } -} - -void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) -{ - QString oldMetaFileName = QString("%1.meta").arg(m_FileName); - m_FileName = QFileInfo(newName).fileName(); - if ((m_State == DownloadManager::STATE_STARTED) || - (m_State == DownloadManager::STATE_DOWNLOADING) || - (m_State == DownloadManager::STATE_PAUSED)) { - newName.append(UNFINISHED); - oldMetaFileName = QString("%1%2.meta").arg(m_FileName).arg(UNFINISHED); - } - if (renameFile) { - if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName)); - return; - } - - QFile metaFile(QFileInfo(newName).path() + "/" + oldMetaFileName); - if (metaFile.exists()) - metaFile.rename(newName.mid(0).append(".meta")); - } - if (!m_Output.isOpen()) { - // can't set file name if it's open - m_Output.setFileName(newName); - } -} - -bool DownloadManager::DownloadInfo::isPausedState() -{ - return m_State == STATE_PAUSED || m_State == STATE_ERROR; -} - -QString DownloadManager::DownloadInfo::currentURL() -{ - return m_Urls[m_CurrentUrl]; -} - - -DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) - : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false), - m_DateExpression("/Date\\((\\d+)\\)/") -{ - connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); - m_TimeoutTimer.setSingleShot(false); - //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout())); - m_TimeoutTimer.start(5 * 1000); -} - - -DownloadManager::~DownloadManager() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - delete *iter; - } - m_ActiveDownloads.clear(); -} - - -bool DownloadManager::downloadsInProgress() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - if ((*iter)->m_State < STATE_READY) { - return true; - } - } - return false; -} - -bool DownloadManager::downloadsInProgressNoPause() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - if ((*iter)->m_State < STATE_READY && (*iter)->m_State != STATE_PAUSED) { - return true; - } - } - return false; -} - - -void DownloadManager::pauseAll() -{ - - // first loop: pause all downloads - for (int i = 0; i < m_ActiveDownloads.count(); ++i) { - if (m_ActiveDownloads[i]->m_State < STATE_READY) { - pauseDownload(i); - } - } - - ::Sleep(100); - - bool done = false; - QTime startTime = QTime::currentTime(); - // further loops: busy waiting for all downloads to complete. This could be neater... - while (!done && (startTime.secsTo(QTime::currentTime()) < 5)) { - QCoreApplication::processEvents(); - done = true; - foreach (DownloadInfo *info, m_ActiveDownloads) { - if ((info->m_State < STATE_CANCELED) || - (info->m_State == STATE_FETCHINGFILEINFO) || - (info->m_State == STATE_FETCHINGMODINFO)) { - done = false; - break; - } - } - if (!done) { - ::Sleep(100); - } - } - -} - - -void DownloadManager::setOutputDirectory(const QString &outputDirectory) -{ - QStringList directories = m_DirWatcher.directories(); - if (directories.length() != 0) { - m_DirWatcher.removePaths(directories); - } - m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory); - refreshList(); - m_DirWatcher.addPath(m_OutputDirectory); -} - - -void DownloadManager::setPreferredServers(const std::map &preferredServers) -{ - m_PreferredServers = preferredServers; -} - - -void DownloadManager::setSupportedExtensions(const QStringList &extensions) -{ - m_SupportedExtensions = extensions; - refreshList(); -} - -void DownloadManager::setShowHidden(bool showHidden) -{ - m_ShowHidden = showHidden; - refreshList(); -} - -void DownloadManager::setPluginContainer(PluginContainer *pluginContainer) -{ - m_NexusInterface->setPluginContainer(pluginContainer); -} - - - - - - -void DownloadManager::refreshList() -{ - try { - //avoid triggering other refreshes - startDisableDirWatcher(); - - int downloadsBefore = m_ActiveDownloads.size(); - - // remove finished downloads - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { - if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) { - delete *iter; - iter = m_ActiveDownloads.erase(iter); - } else { - ++iter; - } - } - - QStringList nameFilters(m_SupportedExtensions); - foreach (const QString &extension, m_SupportedExtensions) { - nameFilters.append("*." + extension); - } - - nameFilters.append(QString("*").append(UNFINISHED)); - QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); - - // find orphaned meta files and delete them (sounds cruel but it's better for everyone) - QStringList orphans; - QStringList metaFiles = dir.entryList(QStringList() << "*.meta"); - foreach (const QString &metaFile, metaFiles) { - QString baseFile = metaFile.left(metaFile.length() - 5); - if (!QFile::exists(dir.absoluteFilePath(baseFile))) { - orphans.append(dir.absoluteFilePath(metaFile)); - } - } - if (orphans.size() > 0) { - qDebug("%d orphaned meta files will be deleted", orphans.size()); - shellDelete(orphans, true); - } - - // add existing downloads to list - foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { - bool Exists = false; - for (QVector::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) { - if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) { - Exists = true; - } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) { - Exists = true; - } - } - if (Exists) { - continue; - } - - QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file; - - DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden, m_OutputDirectory); - if (info != nullptr) { - m_ActiveDownloads.push_front(info); - } - } - - //if (m_ActiveDownloads.size() != downloadsBefore) { - qDebug("Downloads after refresh: %d", m_ActiveDownloads.size()); - //} - emit update(-1); - - //let watcher trigger refreshes again - endDisableDirWatcher(); - - } catch (const std::bad_alloc&) { - reportError(tr("Memory allocation error (in refreshing directory).")); - } -} - - -bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, - int modID, int fileID, const ModRepositoryFileInfo *fileInfo) -{ - QString fileName = QFileInfo(URLs.first()).fileName(); - if (fileName.isEmpty()) { - fileName = "unknown"; - } - - QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit()); - qDebug("selected download url: %s", qUtf8Printable(preferredUrl.toString())); - QNetworkRequest request(preferredUrl); - request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); - return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo); -} - - -bool DownloadManager::addDownload(QNetworkReply *reply, const ModRepositoryFileInfo *fileInfo) -{ - QString fileName = getFileNameFromNetworkReply(reply); - if (fileName.isEmpty()) { - fileName = "unknown"; - } - - return addDownload(reply, QStringList(reply->url().toString()), fileName, fileInfo->gameName, fileInfo->modID, fileInfo->fileID, fileInfo); -} - - -bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, - QString gameName, int modID, int fileID, const ModRepositoryFileInfo *fileInfo) -{ - // download invoked from an already open network reply (i.e. download link in the browser) - DownloadInfo *newDownload = DownloadInfo::createNew(fileInfo, URLs); - - QString baseName = fileName; - if (!fileInfo->fileName.isEmpty()) { - baseName = fileInfo->fileName; - } else { - QString dispoName = getFileNameFromNetworkReply(reply); - - if (!dispoName.isEmpty()) { - baseName = dispoName; - } - } - - startDisableDirWatcher(); - newDownload->setName(getDownloadFileName(baseName), false); - endDisableDirWatcher(); - - startDownload(reply, newDownload, false); -// emit update(-1); - return true; -} - - -void DownloadManager::removePending(QString gameName, int modID, int fileID) -{ - emit aboutToUpdate(); - for (auto iter : m_PendingDownloads) { - if (gameName.compare(std::get<0>(iter), Qt::CaseInsensitive) == 0 && (std::get<1>(iter) == modID) && (std::get<2>(iter) == fileID)) { - m_PendingDownloads.removeAt(m_PendingDownloads.indexOf(iter)); - break; - } - } - emit update(-1); -} - - -void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume) -{ - reply->setReadBufferSize(1024 * 1024); // don't read more than 1MB at once to avoid memory troubles - newDownload->m_Reply = reply; - setState(newDownload, STATE_DOWNLOADING); - if (newDownload->m_Urls.count() == 0) { - newDownload->m_Urls = QStringList(reply->url().toString()); - } - - QIODevice::OpenMode mode = QIODevice::WriteOnly; - if (resume) { - mode |= QIODevice::Append; - } - - newDownload->m_StartTime.start(); - createMetaFile(newDownload); - - if (!newDownload->m_Output.open(mode)) { - reportError(tr("failed to download %1: could not open output file: %2") - .arg(reply->url().toString()).arg(newDownload->m_Output.fileName())); - return; - } - - connect(newDownload->m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64))); - connect(newDownload->m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); - connect(newDownload->m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead())); - connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged())); - - if (!resume) { - newDownload->m_PreResumeSize = newDownload->m_Output.size(); - removePending(newDownload->m_FileInfo->gameName, newDownload->m_FileInfo->modID, newDownload->m_FileInfo->fileID); - - emit aboutToUpdate(); - m_ActiveDownloads.append(newDownload); - - emit update(-1); - emit downloadAdded(); - - if (QFile::exists(m_OutputDirectory + "/" + newDownload->m_FileName)) { - setState(newDownload, STATE_PAUSING); - QCoreApplication::processEvents(); - if (QMessageBox::question(nullptr, tr("Download again?"), tr("A file with the same name \"%1\" has already been downloaded. " - "Do you want to download it again? The new file will receive a different name.").arg(newDownload->m_FileName), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { - if (reply->isFinished()) - setState(newDownload, STATE_CANCELED); - else - setState(newDownload, STATE_CANCELING); - } else { - startDisableDirWatcher(); - newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true); - endDisableDirWatcher(); - if (newDownload->m_State == STATE_PAUSED) - resumeDownload(indexByName(newDownload->m_FileName)); - else - setState(newDownload, STATE_DOWNLOADING); - } - } else - connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); - - - QCoreApplication::processEvents(); - - if (newDownload->m_State != STATE_DOWNLOADING && - newDownload->m_State != STATE_READY && - newDownload->m_State != STATE_FETCHINGMODINFO && - reply->isFinished()) { - downloadFinished(indexByName(newDownload->m_FileName)); - return; - } - } else - connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); -} - - -void DownloadManager::addNXMDownload(const QString &url) -{ - NXMUrl nxmInfo(url); - - QStringList validGames; - validGames.append(m_ManagedGame->gameShortName()); - validGames.append(m_ManagedGame->validShortNames()); - qDebug("add nxm download: %s", qUtf8Printable(url)); - if (!validGames.contains(nxmInfo.game(), Qt::CaseInsensitive)) { - qDebug("download requested for wrong game (game: %s, url: %s)", qUtf8Printable(m_ManagedGame->gameShortName()), qUtf8Printable(nxmInfo.game())); - QMessageBox::information(nullptr, tr("Wrong Game"), tr("The download link is for a mod for \"%1\" but this instance of MO " - "has been set up for \"%2\".").arg(nxmInfo.game()).arg(m_ManagedGame->gameShortName()), QMessageBox::Ok); - return; - } - - for (auto tuple : m_PendingDownloads) { - if (std::get<0>(tuple).compare(nxmInfo.game(), Qt::CaseInsensitive) == 0, std::get<1>(tuple) == nxmInfo.modId() && std::get<2>(tuple) == nxmInfo.fileId()) { - qDebug("download requested is already started (mod id: %s, file id: %s)", qUtf8Printable(QString(nxmInfo.modId())), qUtf8Printable(QString(nxmInfo.fileId()))); - QMessageBox::information(nullptr, tr("Already Started"), tr("A download for this mod file has already been queued."), QMessageBox::Ok); - return; - } - } - - for (DownloadInfo *download : m_ActiveDownloads) { - if (download->m_FileInfo->modID == nxmInfo.modId() && download->m_FileInfo->fileID == nxmInfo.fileId()) { - if (download->m_State == STATE_DOWNLOADING || download->m_State == STATE_PAUSED || download->m_State == STATE_STARTED) { - qDebug("download requested is already started (mod: %s, file: %s)", qUtf8Printable(QString(download->m_FileInfo->modID)), - qUtf8Printable(download->m_FileInfo->fileName)); - - QMessageBox::information(nullptr, tr("Already Started"), tr("There is already a download started for this file (mod: %1, file: %2).") - .arg(download->m_FileInfo->modName).arg(download->m_FileInfo->fileName), QMessageBox::Ok); - return; - } - } - } - - emit aboutToUpdate(); - - m_PendingDownloads.append(std::make_tuple(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId())); - - emit update(-1); - emit downloadAdded(); - m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId(), "")); -} - - -void DownloadManager::removeFile(int index, bool deleteFile) -{ - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - if (index >= m_ActiveDownloads.size()) { - throw MyException(tr("remove: invalid download index %1").arg(index)); - } - - DownloadInfo *download = m_ActiveDownloads.at(index); - QString filePath = m_OutputDirectory + "/" + download->m_FileName; - if ((download->m_State == STATE_STARTED) || - (download->m_State == STATE_DOWNLOADING)) { - // shouldn't have been possible - qCritical("tried to remove active download"); - endDisableDirWatcher(); - return; - } - - if ((download->m_State == STATE_PAUSED) || (download->m_State == STATE_ERROR)) { - filePath = download->m_Output.fileName(); - } - - if (deleteFile) { - if (!shellDelete(QStringList(filePath), true)) { - reportError(tr("failed to delete %1").arg(filePath)); - endDisableDirWatcher(); - return; - } - - QFile metaFile(filePath.append(".meta")); - if (metaFile.exists() && !shellDelete(QStringList(filePath), true)) { - reportError(tr("failed to delete meta file for %1").arg(filePath)); - } - } else { - QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); - if(!download->m_Hidden) - metaSettings.setValue("removed", true); - } - - endDisableDirWatcher(); -} - -class LessThanWrapper -{ -public: - LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {} - bool operator()(int LHS, int RHS) { - return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0; - - } - -private: - DownloadManager *m_Manager; -}; - - -bool DownloadManager::ByName(int LHS, int RHS) -{ - return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName; -} - - -void DownloadManager::refreshAlphabeticalTranslation() -{ - m_AlphabeticalTranslation.clear(); - int pos = 0; - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) { - m_AlphabeticalTranslation.push_back(pos); - } - - qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this)); -} - - -void DownloadManager::restoreDownload(int index) -{ - - if (index < 0) { - DownloadState minState = STATE_READY ; - index = 0; - - for (QVector::const_iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter ) { - - if ((*iter)->m_State >= minState) { - restoreDownload(index); - } - index++; - } - } - else { - if (index >= m_ActiveDownloads.size()) { - throw MyException(tr("restore: invalid download index: %1").arg(index)); - } - - DownloadInfo *download = m_ActiveDownloads.at(index); - if (download->m_Hidden) { - download->m_Hidden = false; - - QString filePath = m_OutputDirectory + "/" + download->m_FileName; - - //avoid dirWatcher triggering refreshes - startDisableDirWatcher(); - QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); - metaSettings.setValue("removed", false); - - endDisableDirWatcher(); - } - } -} - - -void DownloadManager::removeDownload(int index, bool deleteFile) -{ - try { - //avoid dirWatcher triggering refreshes - startDisableDirWatcher(); - - emit aboutToUpdate(); - - if (index < 0) { - bool removeAll = (index == -1); - DownloadState removeState = (index == -2 ? STATE_INSTALLED : STATE_UNINSTALLED); - - index = 0; - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { - DownloadState downloadState = (*iter)->m_State; - if ((removeAll && (downloadState >= STATE_READY)) || - (removeState == downloadState)) { - removeFile(index, deleteFile); - delete *iter; - iter = m_ActiveDownloads.erase(iter); - } else { - ++iter; - ++index; - } - } - } else { - if (index >= m_ActiveDownloads.size()) { - reportError(tr("remove: invalid download index %1").arg(index)); - //emit update(-1); - endDisableDirWatcher(); - return; - } - - removeFile(index, deleteFile); - delete m_ActiveDownloads.at(index); - m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); - } - emit update(-1); - endDisableDirWatcher(); - } catch (const std::exception &e) { - qCritical("failed to remove download: %s", e.what()); - } - refreshList(); -} - - -void DownloadManager::cancelDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("cancel: invalid download index %1").arg(index)); - return; - } - - if (m_ActiveDownloads.at(index)->m_State == STATE_DOWNLOADING) { - setState(m_ActiveDownloads.at(index), STATE_CANCELING); - } -} - - -void DownloadManager::pauseDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("pause: invalid download index %1").arg(index)); - return; - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - - if (info->m_State == STATE_DOWNLOADING) { - if ((info->m_Reply != nullptr) && (info->m_Reply->isRunning())) { - setState(info, STATE_PAUSING); - } else { - setState(info, STATE_PAUSED); - } - } else if ((info->m_State == STATE_FETCHINGMODINFO) || (info->m_State == STATE_FETCHINGFILEINFO)) { - setState(info, STATE_READY); - } -} - -void DownloadManager::resumeDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("resume: invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - info->m_Tries = AUTOMATIC_RETRIES; - resumeDownloadInt(index); -} - -void DownloadManager::resumeDownloadInt(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("resume (int): invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - - // Check for finished download; - if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr - && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { - setState(info, STATE_DOWNLOADING); - downloadFinished(index); - return; - } - - if (info->isPausedState() || info->m_State == STATE_PAUSING) { - if (info->m_State == STATE_PAUSING) { - if (info->m_Output.isOpen()) { - writeData(info); - if (info->m_State == STATE_PAUSING) { - setState(info, STATE_PAUSED); - } - } - } - if ((info->m_Urls.size() == 0) - || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) { - emit showMessage(tr("No known download urls. Sorry, this download can't be resumed.")); - return; - } - if (info->m_State == STATE_ERROR) { - info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count(); - } - qDebug("request resume from url %s", qUtf8Printable(info->currentURL())); - QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit())); - request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); - if (info->m_State != STATE_ERROR) { - info->m_ResumePos = info->m_Output.size(); - QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; - request.setRawHeader("Range", rangeHeader); - } - std::get<0>(info->m_SpeedDiff) = 0; - std::get<1>(info->m_SpeedDiff) = 0; - std::get<2>(info->m_SpeedDiff) = 0; - std::get<3>(info->m_SpeedDiff) = 0; - std::get<4>(info->m_SpeedDiff) = 0; - qDebug("resume at %lld bytes", info->m_ResumePos); - startDownload(m_NexusInterface->getAccessManager()->get(request), info, true); - } - emit update(index); -} - - -DownloadManager::DownloadInfo *DownloadManager::downloadInfoByID(unsigned int id) -{ - auto iter = std::find_if(m_ActiveDownloads.begin(), m_ActiveDownloads.end(), - [id](DownloadInfo *info) { return info->m_DownloadID == id; }); - if (iter != m_ActiveDownloads.end()) { - return *iter; - } else { - return nullptr; - } -} - - -void DownloadManager::queryInfo(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("query: invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - - if (info->m_FileInfo->repository != "Nexus") { - qWarning("re-querying file info is currently only possible with Nexus"); - return; - } - - if (info->m_State < DownloadManager::STATE_READY) { - // UI shouldn't allow this - return; - } - - if (info->m_FileInfo->modID <= 0) { - QString fileName = getFileName(index); - QString ignore; - NexusInterface::interpretNexusFileName(fileName, ignore, info->m_FileInfo->modID, true); - if (info->m_FileInfo->modID < 0) { - bool ok = false; - int modId = QInputDialog::getInt( - nullptr, tr("Please enter the nexus mod id"), tr("Mod ID:"), 1, 1, - std::numeric_limits::max(), 1, &ok); - // careful now: while the dialog was displayed, events were processed. - // the download list might have changed and our info-ptr invalidated. - if (ok) - m_ActiveDownloads[index]->m_FileInfo->modID = modId; - return; - } - } - - if (info->m_FileInfo->gameName.size() == 0) { - SelectionDialog selection(tr("Please select the source game code for %1").arg(getFileName(index))); - - std::vector> choices = m_NexusInterface->getGameChoices(m_ManagedGame); - for (auto choice : choices) { - selection.addChoice(choice.first, choice.second, choice.first); - } - if (selection.exec() == QDialog::Accepted) { - info->m_FileInfo->gameName = selection.getChoiceData().toString(); - } else { - info->m_FileInfo->gameName = m_ManagedGame->gameShortName(); - } - } - info->m_ReQueried = true; - setState(info, STATE_FETCHINGMODINFO); -} - -void DownloadManager::visitOnNexus(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("VisitNexus: invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - - if (info->m_FileInfo->repository != "Nexus") { - qWarning("Visiting mod page is currently only possible with Nexus"); - return; - } - - if (info->m_State < DownloadManager::STATE_READY) { - // UI shouldn't allow this - return; - } - int modID = info->m_FileInfo->modID; - - QString gameName = info->m_FileInfo->gameName; - if (modID > 0) { - QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName))); - } - else { - emit showMessage(tr("Nexus ID for this Mod is unknown")); - } -} - -void DownloadManager::openFile(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("OpenFile: invalid download index %1").arg(index)); - return; - } - QDir path = QDir(m_OutputDirectory); - if (path.exists(getFileName(index))) { - - ::ShellExecuteW(nullptr, L"open", ToWString(QDir::toNativeSeparators(getFilePath(index))).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - return; - } - - ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - return; -} - -void DownloadManager::openInDownloadsFolder(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("OpenFileInDownloadsFolder: invalid download index %1").arg(index)); - return; - } - QString params = "/select,\""; - QDir path = QDir(m_OutputDirectory); - if (path.exists(getFileName(index))) { - params = params + QDir::toNativeSeparators(getFilePath(index)) + "\""; - - ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); - return; - } - else if (path.exists(getFileName(index) + ".unfinished")) { - params = params + QDir::toNativeSeparators(getFilePath(index)+".unfinished") + "\""; - - ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); - return; - } - - ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - return; -} - - -int DownloadManager::numTotalDownloads() const -{ - return m_ActiveDownloads.size(); -} - -int DownloadManager::numPendingDownloads() const -{ - return m_PendingDownloads.size(); -} - -std::tuple DownloadManager::getPendingDownload(int index) -{ - if ((index < 0) || (index >= m_PendingDownloads.size())) { - throw MyException(tr("get pending: invalid download index %1").arg(index)); - } - - return m_PendingDownloads.at(index); -} - -QString DownloadManager::getFilePath(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("get path: invalid download index %1").arg(index)); - } - - return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName; -} - -QString DownloadManager::getFileTypeString(int fileType) -{ - switch (fileType) { - case 1: return tr("Main"); - case 2: return tr("Update"); - case 3: return tr("Optional"); - case 4: return tr("Old"); - case 5: return tr("Misc"); - default: return tr("Unknown"); - } -} - -QString DownloadManager::getDisplayName(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("display name: invalid download index %1").arg(index)); - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - - QTextDocument doc; - if (!info->m_FileInfo->name.isEmpty()) { - doc.setHtml(info->m_FileInfo->name); - return QString("%1 (%2, v%3)").arg(doc.toPlainText()) - .arg(getFileTypeString(info->m_FileInfo->fileCategory)) - .arg(info->m_FileInfo->version.displayString()); - } else { - doc.setHtml(info->m_FileName); - return doc.toPlainText(); - } -} - -QString DownloadManager::getFileName(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file name: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_FileName; -} - -QDateTime DownloadManager::getFileTime(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file time: invalid download index %1").arg(index)); - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - if (!info->m_Created.isValid()) { - info->m_Created = QFileInfo(info->m_Output).created(); - } - - return info->m_Created; -} - -qint64 DownloadManager::getFileSize(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file size: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_TotalSize; -} - - -std::pair DownloadManager::getProgress(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("progress: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_Progress; -} - - -DownloadManager::DownloadState DownloadManager::getState(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("state: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_State; -} - - -bool DownloadManager::isInfoIncomplete(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("infocomplete: invalid download index %1").arg(index)); - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - if (info->m_FileInfo->repository != "Nexus") { - // other repositories currently don't support re-querying info anyway - return false; - } - return (info->m_FileInfo->fileID == 0) || (info->m_FileInfo->modID == 0); -} - - -int DownloadManager::getModID(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mod id: invalid download index %1").arg(index)); - } - return m_ActiveDownloads.at(index)->m_FileInfo->modID; -} - -QString DownloadManager::getGameName(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mod id: invalid download index %1").arg(index)); - } - return m_ActiveDownloads.at(index)->m_FileInfo->gameName; -} - -bool DownloadManager::isHidden(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("ishidden: invalid download index %1").arg(index)); - } - return m_ActiveDownloads.at(index)->m_Hidden; -} - - -const ModRepositoryFileInfo *DownloadManager::getFileInfo(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file info: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_FileInfo; -} - - -void DownloadManager::markInstalled(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mark installed: invalid download index %1").arg(index)); - } - - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - DownloadInfo *info = m_ActiveDownloads.at(index); - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("installed", true); - metaFile.setValue("uninstalled", false); - - endDisableDirWatcher(); - - setState(m_ActiveDownloads.at(index), STATE_INSTALLED); -} - -void DownloadManager::markInstalled(QString fileName) -{ - int index = indexByName(fileName); - if (index >= 0) { - markInstalled(index); - } else { - DownloadInfo *info = getDownloadInfo(fileName); - if (info != nullptr) { - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("installed", true); - metaFile.setValue("uninstalled", false); - delete info; - - endDisableDirWatcher(); - } - } -} - -DownloadManager::DownloadInfo* DownloadManager::getDownloadInfo(QString fileName) -{ - return DownloadInfo::createFromMeta(fileName, true, m_OutputDirectory); -} - -void DownloadManager::markUninstalled(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mark uninstalled: invalid download index %1").arg(index)); - } - - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - DownloadInfo *info = m_ActiveDownloads.at(index); - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("uninstalled", true); - - endDisableDirWatcher(); - - setState(m_ActiveDownloads.at(index), STATE_UNINSTALLED); -} - - -void DownloadManager::markUninstalled(QString fileName) -{ - int index = indexByName(fileName); - if (index >= 0) { - markUninstalled(index); - } else { - QString filePath = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + fileName; - DownloadInfo *info = getDownloadInfo(filePath); - if (info != nullptr) { - - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("uninstalled", true); - delete info; - - endDisableDirWatcher(); - } - } -} - - -QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const -{ - QString fullPath = m_OutputDirectory + "/" + baseName; - if (QFile::exists(fullPath) && rename) { - int i = 1; - while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) { - ++i; - } - - fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName); - } - return fullPath; -} - - -QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply) -{ - if (reply->hasRawHeader("Content-Disposition")) { - std::regex exp("filename=\"(.*)\""); - - std::cmatch result; - if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) { - return QString::fromUtf8(result.str(1).c_str()); - } - } - - return QString(); -} - - -void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadManager::DownloadState state) -{ - int row = 0; - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i] == info) { - row = i; - break; - } - } - info->m_State = state; - switch (state) { - case STATE_PAUSED: - case STATE_ERROR: { - info->m_Reply->abort(); - info->m_Output.close(); - } break; - case STATE_CANCELED: { - info->m_Reply->abort(); - } break; - case STATE_FETCHINGMODINFO: { - m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); - } break; - case STATE_FETCHINGFILEINFO: { - m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); - } break; - case STATE_READY: { - createMetaFile(info); - emit downloadComplete(row); - } break; - default: /* NOP */ break; - } - emit stateChanged(row, state); -} - - -DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const -{ - // reverse search as newer, thus more relevant, downloads are at the end - for (int i = m_ActiveDownloads.size() - 1; i >= 0; --i) { - if (m_ActiveDownloads[i]->m_Reply == reply) { - if (index != nullptr) { - *index = i; - } - return m_ActiveDownloads[i]; - } - } - return nullptr; -} - - -void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - if (bytesTotal == 0) { - return; - } - int index = 0; - try { - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != nullptr) { - info->m_HasData = true; - if (info->m_State == STATE_CANCELING) { - setState(info, STATE_CANCELED); - } else if (info->m_State == STATE_PAUSING) { - setState(info, STATE_PAUSED); - } - else { - if (bytesTotal > info->m_TotalSize) { - info->m_TotalSize = bytesTotal; - } - int oldProgress = info->m_Progress.first; - info->m_Progress.first = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); - - int elapsed = info->m_StartTime.elapsed(); - std::get<0>(info->m_SpeedDiff) = bytesReceived - std::get<2>(info->m_SpeedDiff); - std::get<1>(info->m_SpeedDiff) = elapsed - std::get<3>(info->m_SpeedDiff); - std::get<2>(info->m_SpeedDiff) = bytesReceived; - std::get<3>(info->m_SpeedDiff) = elapsed; - - double calc = ((double)std::get<0>(info->m_SpeedDiff)) / (((double)(std::get<1>(info->m_SpeedDiff)) / 5000.0)); - std::get<4>(info->m_SpeedDiff) = ((calc*0.5) + (std::get<4>(info->m_SpeedDiff)*1.5)) / 2; - - // calculate the download speed - double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000); - - QString unit; - if (speed < 1000) { - unit = "B/s"; - } - else if (speed < 1000*1024) { - speed /= 1024; - unit = "KB/s"; - } - else { - speed /= 1024 * 1024; - unit = "MB/s"; - } - - info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(QString::number(speed, 'f', 1)).arg(unit); - - TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal); - emit update(index); - } - } - } catch (const std::bad_alloc&) { - reportError(tr("Memory allocation error (in processing progress event).")); - } -} - - -void DownloadManager::downloadReadyRead() -{ - try { - writeData(findDownload(this->sender())); - } catch (const std::bad_alloc&) { - reportError(tr("Memory allocation error (in processing downloaded data).")); - } -} - - -void DownloadManager::createMetaFile(DownloadInfo *info) -{ - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat); - metaFile.setValue("gameName", info->m_FileInfo->gameName); - metaFile.setValue("modID", info->m_FileInfo->modID); - metaFile.setValue("fileID", info->m_FileInfo->fileID); - metaFile.setValue("url", info->m_Urls.join(";")); - metaFile.setValue("name", info->m_FileInfo->name); - metaFile.setValue("description", info->m_FileInfo->description); - metaFile.setValue("modName", info->m_FileInfo->modName); - metaFile.setValue("version", info->m_FileInfo->version.canonicalString()); - metaFile.setValue("newestVersion", info->m_FileInfo->newestVersion.canonicalString()); - metaFile.setValue("fileTime", info->m_FileInfo->fileTime); - metaFile.setValue("fileCategory", info->m_FileInfo->fileCategory); - metaFile.setValue("category", info->m_FileInfo->categoryID); - metaFile.setValue("repository", info->m_FileInfo->repository); - metaFile.setValue("userData", info->m_FileInfo->userData); - metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED); - metaFile.setValue("uninstalled", info->m_State == DownloadManager::STATE_UNINSTALLED); - metaFile.setValue("paused", (info->m_State == DownloadManager::STATE_PAUSED) || - (info->m_State == DownloadManager::STATE_ERROR)); - metaFile.setValue("removed", info->m_Hidden); - - endDisableDirWatcher(); - // slightly hackish... - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i] == info) { - emit update(i); - } - } -} - - -void DownloadManager::nxmDescriptionAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - QVariantMap result = resultData.toMap(); - - DownloadInfo *info = downloadInfoByID(userData.toInt()); - if (info == nullptr) return; - info->m_FileInfo->categoryID = result["category_id"].toInt(); - QTextDocument doc; - doc.setHtml(result["name"].toString().trimmed()); - info->m_FileInfo->modName = doc.toPlainText(); - info->m_FileInfo->newestVersion.parse(result["version"].toString()); - if (info->m_FileInfo->fileID != 0) { - setState(info, STATE_READY); - } else { - setState(info, STATE_FETCHINGFILEINFO); - } -} - - -QDateTime DownloadManager::matchDate(const QString &timeString) -{ - if (m_DateExpression.exactMatch(timeString)) { - return QDateTime::fromMSecsSinceEpoch(m_DateExpression.cap(1).toLongLong()); - } else { - qWarning("date not matched: %s", qUtf8Printable(timeString)); - return QDateTime::currentDateTime(); - } -} - - -void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - DownloadInfo *info = downloadInfoByID(userData.toInt()); - if (info == nullptr) return; - - QVariantList result = resultData.toList(); - - // MO sometimes prepends _ to the filename in case of duplicate downloads. - // this may muck up the file name comparison - QString alternativeLocalName = info->m_FileName; - - QRegExp expression("^\\d_(.*)$"); - if (expression.indexIn(alternativeLocalName) == 0) { - alternativeLocalName = expression.cap(1); - } - - bool found = false; - - for (QVariant file : result) { - QVariantMap fileInfo = file.toMap(); - QString fileName = fileInfo["uri"].toString(); - QString fileNameVariant = fileName.mid(0).replace(' ', '_'); - if ((fileName == info->m_FileName) || (fileName == alternativeLocalName) || - (fileNameVariant == info->m_FileName) || (fileNameVariant == alternativeLocalName)) { - info->m_FileInfo->name = fileInfo["name"].toString(); - info->m_FileInfo->version.parse(fileInfo["version"].toString()); - if (!info->m_FileInfo->version.isValid()) { - info->m_FileInfo->version = info->m_FileInfo->newestVersion; - } - info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); - info->m_FileInfo->fileTime = matchDate(fileInfo["date"].toString()); - info->m_FileInfo->fileID = fileInfo["id"].toInt(); - info->m_FileInfo->fileName = fileInfo["uri"].toString(); - info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["description"].toString()); - found = true; - break; - } - } - - if (info->m_ReQueried) { - if (found) { - emit showMessage(tr("Information updated")); - } else if (result.count() == 0) { - emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); - } else { - SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one.")); - for (QVariant file : result) { - QVariantMap fileInfo = file.toMap(); - selection.addChoice(fileInfo["uri"].toString(), "", file); - } - if (selection.exec() == QDialog::Accepted) { - QVariantMap fileInfo = selection.getChoiceData().toMap(); - info->m_FileInfo->name = fileInfo["name"].toString(); - info->m_FileInfo->version.parse(fileInfo["version"].toString()); - info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); - info->m_FileInfo->fileID = fileInfo["id"].toInt(); - } else { - emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); - } - } - } else { - if (info->m_FileInfo->fileID == 0) { - qWarning("could not determine file id for %s (state %d)", - qUtf8Printable(info->m_FileName), info->m_State); - } - } - - setState(info, STATE_READY); -} - - -void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - ModRepositoryFileInfo *info = new ModRepositoryFileInfo(); - - QVariantMap result = resultData.toMap(); - info->name = result["name"].toString(); - info->version.parse(result["version"].toString()); - if (!info->version.isValid()) { - info->version = info->newestVersion; - } - info->fileName = result["file_name"].toString(); - info->fileCategory = result["category_id"].toInt(); - info->fileTime = matchDate(result["uploaded_timestamp"].toString()); - info->description = BBCode::convertToHTML(result["changelog_html"].toString()); - - info->repository = "Nexus"; - info->gameName = gameName; - info->modID = modID; - info->fileID = fileID; - - QObject *test = info; - m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(gameName, modID, fileID, this, qVariantFromValue(test), QString())); -} - -static int evaluateFileInfoMap(const QVariantMap &map, const std::map &preferredServers) -{ - int result = 0; - - auto preference = preferredServers.find(map["name"].toString()); - - if (preference != preferredServers.end()) { - result += 100 + preference->second * 20; - } - - return result; -} - -// sort function to sort by best download server -bool DownloadManager::ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS) -{ - return evaluateFileInfoMap(LHS.toMap(), preferredServers) > evaluateFileInfoMap(RHS.toMap(), preferredServers); -} - -int DownloadManager::startDownloadURLs(const QStringList &urls) -{ - ModRepositoryFileInfo info; - addDownload(urls, "", -1, -1, &info); - return m_ActiveDownloads.size() - 1; -} - -int DownloadManager::startDownloadNexusFile(int modID, int fileID) -{ - int newID = m_ActiveDownloads.size(); - addNXMDownload(QString("nxm://%1/mods/%2/files/%3").arg(m_ManagedGame->gameShortName()).arg(modID).arg(fileID)); - return newID; -} - -QString DownloadManager::downloadPath(int id) -{ - return getFilePath(id); -} - -int DownloadManager::indexByName(const QString &fileName) const -{ - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i]->m_FileName == fileName) { - return i; - } - } - return -1; -} - -void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - ModRepositoryFileInfo *info = qobject_cast(qvariant_cast(userData)); - QVariantList resultList = resultData.toList(); - if (resultList.length() == 0) { - removePending(gameName, modID, fileID); - emit showMessage(tr("No download server available. Please try again later.")); - return; - } - - std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2)); - - info->userData["downloadMap"] = resultList; - - QStringList URLs; - - foreach (const QVariant &server, resultList) { - URLs.append(server.toMap()["URI"].toString()); - } - addDownload(URLs, gameName, modID, fileID, info); -} - - -void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - int index = 0; - - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { - DownloadInfo *info = *iter; - if (info->m_FileInfo->modID == modID) { - if (info->m_State < STATE_FETCHINGMODINFO) { - m_ActiveDownloads.erase(iter); - delete info; - } else { - setState(info, STATE_READY); - } - emit update(index); - break; - } - } - - removePending(gameName, modID, fileID); - emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString)); -} - - -void DownloadManager::downloadFinished(int index) -{ - DownloadInfo *info; - if (index) - info = m_ActiveDownloads[index]; - else - info = findDownload(this->sender(), &index); - - if (info != nullptr) { - QNetworkReply *reply = info->m_Reply; - QByteArray data; - if (reply->isOpen() && info->m_HasData) { - data = reply->readAll(); - info->m_Output.write(data); - } - info->m_Output.close(); - TaskProgressManager::instance().forgetMe(info->m_TaskProgressId); - - bool error = false; - if ((info->m_State != STATE_CANCELING) && - (info->m_State != STATE_PAUSING)) { - bool textData = reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive); - if (textData) - emit showMessage(tr("Warning: Content type is: %1").arg(reply->header(QNetworkRequest::ContentTypeHeader).toString())); - if ((info->m_Output.size() == 0) || - ((reply->error() != QNetworkReply::NoError) - && (reply->error() != QNetworkReply::OperationCanceledError))) { - if (reply->error() == QNetworkReply::UnknownContentError) - emit showMessage(tr("Download header content length: %1 downloaded file size: %2").arg(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()).arg(info->m_Output.size())); - if (info->m_Tries == 0) { - emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); - } - error = true; - setState(info, STATE_ERROR); - } - } - - if (info->m_State == STATE_CANCELING) { - setState(info, STATE_CANCELED); - } else if (info->m_State == STATE_PAUSING) { - if (info->m_Output.isOpen() && info->m_HasData) { - info->m_Output.write(info->m_Reply->readAll()); - } - setState(info, STATE_PAUSED); - } - - if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) { - emit aboutToUpdate(); - info->m_Output.remove(); - delete info; - m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); - if (error) - emit showMessage(tr("We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.")); - emit update(-1); - } else if (info->isPausedState() || info->m_State == STATE_PAUSING) { - info->m_Output.close(); - createMetaFile(info); - emit update(index); - } else { - QString url = info->m_Urls[info->m_CurrentUrl]; - if (info->m_FileInfo->userData.contains("downloadMap")) { - foreach (const QVariant &server, info->m_FileInfo->userData["downloadMap"].toList()) { - QVariantMap serverMap = server.toMap(); - if (serverMap["URI"].toString() == url) { - int deltaTime = info->m_StartTime.secsTo(QTime::currentTime()); - if (deltaTime > 5) { - emit downloadSpeed(serverMap["Name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime); - } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise - break; - } - } - } - - bool isNexus = info->m_FileInfo->repository == "Nexus"; - // need to change state before changing the file name, otherwise .unfinished is appended - if (isNexus) { - setState(info, STATE_FETCHINGMODINFO); - } else { - setState(info, STATE_NOFETCH); - } - - QString newName = getFileNameFromNetworkReply(reply); - QString oldName = QFileInfo(info->m_Output).fileName(); - - startDisableDirWatcher(); - if (!newName.isEmpty() && (oldName.isEmpty())) { - info->setName(getDownloadFileName(newName), true); - } else { - info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension - } - endDisableDirWatcher(); - - if (!isNexus) { - setState(info, STATE_READY); - } - - emit update(index); - } - reply->close(); - reply->deleteLater(); - - if ((info->m_Tries > 0) && error) { - --info->m_Tries; - resumeDownloadInt(index); - } - } else { - qWarning("no download index %d", index); - } -} - - -void DownloadManager::downloadError(QNetworkReply::NetworkError error) -{ - if (error != QNetworkReply::OperationCanceledError) { - QNetworkReply *reply = qobject_cast(sender()); - qWarning("%s (%d)", reply != nullptr ? qUtf8Printable(reply->errorString()) - : "Download error occured", - error); - } -} - - -void DownloadManager::metaDataChanged() -{ - int index = 0; - - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != nullptr) { - QString newName = getFileNameFromNetworkReply(info->m_Reply); - if (!newName.isEmpty() && (info->m_FileName.isEmpty())) { - startDisableDirWatcher(); - info->setName(getDownloadFileName(newName), true); - endDisableDirWatcher(); - refreshAlphabeticalTranslation(); - if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { - reportError(tr("failed to re-open %1").arg(info->m_FileName)); - setState(info, STATE_CANCELING); - } - } - } else { - qWarning("meta data event for unknown download"); - } -} - -void DownloadManager::directoryChanged(const QString&) -{ - if(DownloadManager::m_DirWatcherDisabler==0) - refreshList(); -} - -void DownloadManager::managedGameChanged(MOBase::IPluginGame const *managedGame) -{ - m_ManagedGame = managedGame; -} - -void DownloadManager::checkDownloadTimeout() -{ - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i]->m_StartTime.elapsed() - std::get<3>(m_ActiveDownloads[i]->m_SpeedDiff) > 5 * 1000 && - m_ActiveDownloads[i]->m_State == STATE_DOWNLOADING && m_ActiveDownloads[i]->m_Reply != nullptr && - m_ActiveDownloads[i]->m_Reply->isOpen()) { - pauseDownload(i); - downloadFinished(i); - resumeDownload(i); - } - } -} - -void DownloadManager::writeData(DownloadInfo *info) -{ - if (info != nullptr) { - qint64 ret = info->m_Output.write(info->m_Reply->readAll()); - if (ret < info->m_Reply->size()) { - QString fileName = info->m_FileName; // m_FileName may be destroyed after setState - setState(info, DownloadState::STATE_CANCELED); - qCritical(QString("Unable to write download \"%2\" to drive (return %1)").arg(ret).arg(info->m_FileName).toLocal8Bit()); - reportError(tr("Unable to write download to drive (return %1).\n" - "Check the drive's available storage.\n\n" - "Canceling download \"%2\"...").arg(ret).arg(fileName)); - } - } -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "downloadmanager.h" + +#include "nxmurl.h" +#include "nexusinterface.h" +#include "nxmaccessmanager.h" +#include "iplugingame.h" +#include "downloadmanager.h" +#include +#include +#include "utility.h" +#include "selectiondialog.h" +#include "bbcode.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +using namespace MOBase; + + +// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads + + +static const char UNFINISHED[] = ".unfinished"; + +unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U; +int DownloadManager::m_DirWatcherDisabler = 0; + + +DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs) +{ + DownloadInfo *info = new DownloadInfo; + info->m_DownloadID = s_NextDownloadID++; + info->m_StartTime.start(); + info->m_PreResumeSize = 0LL; + info->m_Progress = std::make_pair(0, "0.0 B/s "); + info->m_ResumePos = 0; + info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo); + info->m_Urls = URLs; + info->m_CurrentUrl = 0; + info->m_Tries = AUTOMATIC_RETRIES; + info->m_State = STATE_STARTED; + info->m_TaskProgressId = TaskProgressManager::instance().getId(); + info->m_Reply = nullptr; + + return info; +} + +DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory) +{ + DownloadInfo *info = new DownloadInfo; + + QString metaFileName = filePath + ".meta"; + QFileInfo metaFileInfo(metaFileName); + if (QDir::fromNativeSeparators(metaFileInfo.path()).compare(QDir::fromNativeSeparators(outputDirectory), Qt::CaseInsensitive) != 0) return nullptr; + QSettings metaFile(metaFileName, QSettings::IniFormat); + if (!showHidden && metaFile.value("removed", false).toBool()) { + return nullptr; + } else { + info->m_Hidden = metaFile.value("removed", false).toBool(); + } + + QString fileName = QFileInfo(filePath).fileName(); + + if (fileName.endsWith(UNFINISHED)) { + info->m_FileName = fileName.mid( + 0, fileName.length() - static_cast(strlen(UNFINISHED))); + info->m_State = STATE_PAUSED; + } else { + info->m_FileName = fileName; + + if (metaFile.value("paused", false).toBool()) { + info->m_State = STATE_PAUSED; + } else if (metaFile.value("uninstalled", false).toBool()) { + info->m_State = STATE_UNINSTALLED; + } else if (metaFile.value("installed", false).toBool()) { + info->m_State = STATE_INSTALLED; + } else { + info->m_State = STATE_READY; + } + } + + info->m_DownloadID = s_NextDownloadID++; + info->m_Output.setFileName(filePath); + info->m_TotalSize = QFileInfo(filePath).size(); + info->m_PreResumeSize = info->m_TotalSize; + info->m_CurrentUrl = 0; + info->m_Urls = metaFile.value("url", "").toString().split(";"); + info->m_Tries = 0; + info->m_TaskProgressId = TaskProgressManager::instance().getId(); + QString gameName = metaFile.value("gameName", "").toString(); + int modID = metaFile.value("modID", 0).toInt(); + int fileID = metaFile.value("fileID", 0).toInt(); + info->m_FileInfo = new ModRepositoryFileInfo(gameName, modID, fileID); + info->m_FileInfo->name = metaFile.value("name", "").toString(); + if (info->m_FileInfo->name == "0") { + // bug in earlier version + info->m_FileInfo->name = ""; + } + info->m_FileInfo->modName = metaFile.value("modName", "").toString(); + info->m_FileInfo->gameName = gameName; + info->m_FileInfo->modID = modID; + info->m_FileInfo->fileID = fileID; + info->m_FileInfo->description = metaFile.value("description").toString(); + info->m_FileInfo->version.parse(metaFile.value("version", "0").toString()); + info->m_FileInfo->newestVersion.parse(metaFile.value("newestVersion", "0").toString()); + info->m_FileInfo->categoryID = metaFile.value("category", 0).toInt(); + info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt(); + info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString(); + info->m_FileInfo->userData = metaFile.value("userData").toMap(); + info->m_Reply = nullptr; + + return info; +} + +void DownloadManager::startDisableDirWatcher() +{ + DownloadManager::m_DirWatcherDisabler++; +} + + +void DownloadManager::endDisableDirWatcher() +{ + if (DownloadManager::m_DirWatcherDisabler > 0) + { + if (DownloadManager::m_DirWatcherDisabler == 1) + QCoreApplication::processEvents(); + DownloadManager::m_DirWatcherDisabler--; + } + else { + DownloadManager::m_DirWatcherDisabler = 0; + } +} + +void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) +{ + QString oldMetaFileName = QString("%1.meta").arg(m_FileName); + m_FileName = QFileInfo(newName).fileName(); + if ((m_State == DownloadManager::STATE_STARTED) || + (m_State == DownloadManager::STATE_DOWNLOADING) || + (m_State == DownloadManager::STATE_PAUSED)) { + newName.append(UNFINISHED); + oldMetaFileName = QString("%1%2.meta").arg(m_FileName).arg(UNFINISHED); + } + if (renameFile) { + if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName)); + return; + } + + QFile metaFile(QFileInfo(newName).path() + "/" + oldMetaFileName); + if (metaFile.exists()) + metaFile.rename(newName.mid(0).append(".meta")); + } + if (!m_Output.isOpen()) { + // can't set file name if it's open + m_Output.setFileName(newName); + } +} + +bool DownloadManager::DownloadInfo::isPausedState() +{ + return m_State == STATE_PAUSED || m_State == STATE_ERROR; +} + +QString DownloadManager::DownloadInfo::currentURL() +{ + return m_Urls[m_CurrentUrl]; +} + + +DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) + : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false) +{ + connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); + m_TimeoutTimer.setSingleShot(false); + //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout())); + m_TimeoutTimer.start(5 * 1000); +} + + +DownloadManager::~DownloadManager() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + delete *iter; + } + m_ActiveDownloads.clear(); +} + + +bool DownloadManager::downloadsInProgress() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + if ((*iter)->m_State < STATE_READY) { + return true; + } + } + return false; +} + +bool DownloadManager::downloadsInProgressNoPause() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + if ((*iter)->m_State < STATE_READY && (*iter)->m_State != STATE_PAUSED) { + return true; + } + } + return false; +} + + +void DownloadManager::pauseAll() +{ + + // first loop: pause all downloads + for (int i = 0; i < m_ActiveDownloads.count(); ++i) { + if (m_ActiveDownloads[i]->m_State < STATE_READY) { + pauseDownload(i); + } + } + + ::Sleep(100); + + bool done = false; + QTime startTime = QTime::currentTime(); + // further loops: busy waiting for all downloads to complete. This could be neater... + while (!done && (startTime.secsTo(QTime::currentTime()) < 5)) { + QCoreApplication::processEvents(); + done = true; + foreach (DownloadInfo *info, m_ActiveDownloads) { + if ((info->m_State < STATE_CANCELED) || + (info->m_State == STATE_FETCHINGFILEINFO) || + (info->m_State == STATE_FETCHINGMODINFO)) { + done = false; + break; + } + } + if (!done) { + ::Sleep(100); + } + } + +} + + +void DownloadManager::setOutputDirectory(const QString &outputDirectory) +{ + QStringList directories = m_DirWatcher.directories(); + if (directories.length() != 0) { + m_DirWatcher.removePaths(directories); + } + m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory); + refreshList(); + m_DirWatcher.addPath(m_OutputDirectory); +} + + +void DownloadManager::setPreferredServers(const std::map &preferredServers) +{ + m_PreferredServers = preferredServers; +} + + +void DownloadManager::setSupportedExtensions(const QStringList &extensions) +{ + m_SupportedExtensions = extensions; + refreshList(); +} + +void DownloadManager::setShowHidden(bool showHidden) +{ + m_ShowHidden = showHidden; + refreshList(); +} + +void DownloadManager::setPluginContainer(PluginContainer *pluginContainer) +{ + m_NexusInterface->setPluginContainer(pluginContainer); +} + + + + + + +void DownloadManager::refreshList() +{ + try { + //avoid triggering other refreshes + startDisableDirWatcher(); + + int downloadsBefore = m_ActiveDownloads.size(); + + // remove finished downloads + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { + if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) { + delete *iter; + iter = m_ActiveDownloads.erase(iter); + } else { + ++iter; + } + } + + QStringList nameFilters(m_SupportedExtensions); + foreach (const QString &extension, m_SupportedExtensions) { + nameFilters.append("*." + extension); + } + + nameFilters.append(QString("*").append(UNFINISHED)); + QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); + + // find orphaned meta files and delete them (sounds cruel but it's better for everyone) + QStringList orphans; + QStringList metaFiles = dir.entryList(QStringList() << "*.meta"); + foreach (const QString &metaFile, metaFiles) { + QString baseFile = metaFile.left(metaFile.length() - 5); + if (!QFile::exists(dir.absoluteFilePath(baseFile))) { + orphans.append(dir.absoluteFilePath(metaFile)); + } + } + if (orphans.size() > 0) { + qDebug("%d orphaned meta files will be deleted", orphans.size()); + shellDelete(orphans, true); + } + + // add existing downloads to list + foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { + bool Exists = false; + for (QVector::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) { + if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) { + Exists = true; + } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) { + Exists = true; + } + } + if (Exists) { + continue; + } + + QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file; + + DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden, m_OutputDirectory); + if (info != nullptr) { + m_ActiveDownloads.push_front(info); + } + } + + //if (m_ActiveDownloads.size() != downloadsBefore) { + qDebug("Downloads after refresh: %d", m_ActiveDownloads.size()); + //} + emit update(-1); + + //let watcher trigger refreshes again + endDisableDirWatcher(); + + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in refreshing directory).")); + } +} + + +bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, + int modID, int fileID, const ModRepositoryFileInfo *fileInfo) +{ + QString fileName = QFileInfo(URLs.first()).fileName(); + if (fileName.isEmpty()) { + fileName = "unknown"; + } + + QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit()); + qDebug("selected download url: %s", qUtf8Printable(preferredUrl.toString())); + QNetworkRequest request(preferredUrl); + request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); + return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo); +} + + +bool DownloadManager::addDownload(QNetworkReply *reply, const ModRepositoryFileInfo *fileInfo) +{ + QString fileName = getFileNameFromNetworkReply(reply); + if (fileName.isEmpty()) { + fileName = "unknown"; + } + + return addDownload(reply, QStringList(reply->url().toString()), fileName, fileInfo->gameName, fileInfo->modID, fileInfo->fileID, fileInfo); +} + + +bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, + QString gameName, int modID, int fileID, const ModRepositoryFileInfo *fileInfo) +{ + // download invoked from an already open network reply (i.e. download link in the browser) + DownloadInfo *newDownload = DownloadInfo::createNew(fileInfo, URLs); + + QString baseName = fileName; + if (!fileInfo->fileName.isEmpty()) { + baseName = fileInfo->fileName; + } else { + QString dispoName = getFileNameFromNetworkReply(reply); + + if (!dispoName.isEmpty()) { + baseName = dispoName; + } + } + + startDisableDirWatcher(); + newDownload->setName(getDownloadFileName(baseName), false); + endDisableDirWatcher(); + + startDownload(reply, newDownload, false); +// emit update(-1); + return true; +} + + +void DownloadManager::removePending(QString gameName, int modID, int fileID) +{ + emit aboutToUpdate(); + for (auto iter : m_PendingDownloads) { + if (gameName.compare(std::get<0>(iter), Qt::CaseInsensitive) == 0 && (std::get<1>(iter) == modID) && (std::get<2>(iter) == fileID)) { + m_PendingDownloads.removeAt(m_PendingDownloads.indexOf(iter)); + break; + } + } + emit update(-1); +} + + +void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume) +{ + reply->setReadBufferSize(1024 * 1024); // don't read more than 1MB at once to avoid memory troubles + newDownload->m_Reply = reply; + setState(newDownload, STATE_DOWNLOADING); + if (newDownload->m_Urls.count() == 0) { + newDownload->m_Urls = QStringList(reply->url().toString()); + } + + QIODevice::OpenMode mode = QIODevice::WriteOnly; + if (resume) { + mode |= QIODevice::Append; + } + + newDownload->m_StartTime.start(); + createMetaFile(newDownload); + + if (!newDownload->m_Output.open(mode)) { + reportError(tr("failed to download %1: could not open output file: %2") + .arg(reply->url().toString()).arg(newDownload->m_Output.fileName())); + return; + } + + connect(newDownload->m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64))); + connect(newDownload->m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(newDownload->m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead())); + connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged())); + + if (!resume) { + newDownload->m_PreResumeSize = newDownload->m_Output.size(); + removePending(newDownload->m_FileInfo->gameName, newDownload->m_FileInfo->modID, newDownload->m_FileInfo->fileID); + + emit aboutToUpdate(); + m_ActiveDownloads.append(newDownload); + + emit update(-1); + emit downloadAdded(); + + if (QFile::exists(m_OutputDirectory + "/" + newDownload->m_FileName)) { + setState(newDownload, STATE_PAUSING); + QCoreApplication::processEvents(); + if (QMessageBox::question(nullptr, tr("Download again?"), tr("A file with the same name \"%1\" has already been downloaded. " + "Do you want to download it again? The new file will receive a different name.").arg(newDownload->m_FileName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + if (reply->isFinished()) + setState(newDownload, STATE_CANCELED); + else + setState(newDownload, STATE_CANCELING); + } else { + startDisableDirWatcher(); + newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true); + endDisableDirWatcher(); + if (newDownload->m_State == STATE_PAUSED) + resumeDownload(indexByName(newDownload->m_FileName)); + else + setState(newDownload, STATE_DOWNLOADING); + } + } else + connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); + + + QCoreApplication::processEvents(); + + if (newDownload->m_State != STATE_DOWNLOADING && + newDownload->m_State != STATE_READY && + newDownload->m_State != STATE_FETCHINGMODINFO && + reply->isFinished()) { + downloadFinished(indexByName(newDownload->m_FileName)); + return; + } + } else + connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + + +void DownloadManager::addNXMDownload(const QString &url) +{ + NXMUrl nxmInfo(url); + + QStringList validGames; + validGames.append(m_ManagedGame->gameShortName()); + validGames.append(m_ManagedGame->validShortNames()); + qDebug("add nxm download: %s", qUtf8Printable(url)); + if (!validGames.contains(nxmInfo.game(), Qt::CaseInsensitive)) { + qDebug("download requested for wrong game (game: %s, url: %s)", qUtf8Printable(m_ManagedGame->gameShortName()), qUtf8Printable(nxmInfo.game())); + QMessageBox::information(nullptr, tr("Wrong Game"), tr("The download link is for a mod for \"%1\" but this instance of MO " + "has been set up for \"%2\".").arg(nxmInfo.game()).arg(m_ManagedGame->gameShortName()), QMessageBox::Ok); + return; + } + + for (auto tuple : m_PendingDownloads) { + if (std::get<0>(tuple).compare(nxmInfo.game(), Qt::CaseInsensitive) == 0, std::get<1>(tuple) == nxmInfo.modId() && std::get<2>(tuple) == nxmInfo.fileId()) { + qDebug("download requested is already started (mod id: %s, file id: %s)", qUtf8Printable(QString(nxmInfo.modId())), qUtf8Printable(QString(nxmInfo.fileId()))); + QMessageBox::information(nullptr, tr("Already Started"), tr("A download for this mod file has already been queued."), QMessageBox::Ok); + return; + } + } + + for (DownloadInfo *download : m_ActiveDownloads) { + if (download->m_FileInfo->modID == nxmInfo.modId() && download->m_FileInfo->fileID == nxmInfo.fileId()) { + if (download->m_State == STATE_DOWNLOADING || download->m_State == STATE_PAUSED || download->m_State == STATE_STARTED) { + qDebug("download requested is already started (mod: %s, file: %s)", qUtf8Printable(QString(download->m_FileInfo->modID)), + qUtf8Printable(download->m_FileInfo->fileName)); + + QMessageBox::information(nullptr, tr("Already Started"), tr("There is already a download started for this file (mod: %1, file: %2).") + .arg(download->m_FileInfo->modName).arg(download->m_FileInfo->fileName), QMessageBox::Ok); + return; + } + } + } + + emit aboutToUpdate(); + + m_PendingDownloads.append(std::make_tuple(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId())); + + emit update(-1); + emit downloadAdded(); + m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId(), "")); +} + + +void DownloadManager::removeFile(int index, bool deleteFile) +{ + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + if (index >= m_ActiveDownloads.size()) { + throw MyException(tr("remove: invalid download index %1").arg(index)); + } + + DownloadInfo *download = m_ActiveDownloads.at(index); + QString filePath = m_OutputDirectory + "/" + download->m_FileName; + if ((download->m_State == STATE_STARTED) || + (download->m_State == STATE_DOWNLOADING)) { + // shouldn't have been possible + qCritical("tried to remove active download"); + endDisableDirWatcher(); + return; + } + + if ((download->m_State == STATE_PAUSED) || (download->m_State == STATE_ERROR)) { + filePath = download->m_Output.fileName(); + } + + if (deleteFile) { + if (!shellDelete(QStringList(filePath), true)) { + reportError(tr("failed to delete %1").arg(filePath)); + endDisableDirWatcher(); + return; + } + + QFile metaFile(filePath.append(".meta")); + if (metaFile.exists() && !shellDelete(QStringList(filePath), true)) { + reportError(tr("failed to delete meta file for %1").arg(filePath)); + } + } else { + QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); + if(!download->m_Hidden) + metaSettings.setValue("removed", true); + } + + endDisableDirWatcher(); +} + +class LessThanWrapper +{ +public: + LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {} + bool operator()(int LHS, int RHS) { + return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0; + + } + +private: + DownloadManager *m_Manager; +}; + + +bool DownloadManager::ByName(int LHS, int RHS) +{ + return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName; +} + + +void DownloadManager::refreshAlphabeticalTranslation() +{ + m_AlphabeticalTranslation.clear(); + int pos = 0; + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) { + m_AlphabeticalTranslation.push_back(pos); + } + + qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this)); +} + + +void DownloadManager::restoreDownload(int index) +{ + + if (index < 0) { + DownloadState minState = STATE_READY ; + index = 0; + + for (QVector::const_iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter ) { + + if ((*iter)->m_State >= minState) { + restoreDownload(index); + } + index++; + } + } + else { + if (index >= m_ActiveDownloads.size()) { + throw MyException(tr("restore: invalid download index: %1").arg(index)); + } + + DownloadInfo *download = m_ActiveDownloads.at(index); + if (download->m_Hidden) { + download->m_Hidden = false; + + QString filePath = m_OutputDirectory + "/" + download->m_FileName; + + //avoid dirWatcher triggering refreshes + startDisableDirWatcher(); + QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); + metaSettings.setValue("removed", false); + + endDisableDirWatcher(); + } + } +} + + +void DownloadManager::removeDownload(int index, bool deleteFile) +{ + try { + //avoid dirWatcher triggering refreshes + startDisableDirWatcher(); + + emit aboutToUpdate(); + + if (index < 0) { + bool removeAll = (index == -1); + DownloadState removeState = (index == -2 ? STATE_INSTALLED : STATE_UNINSTALLED); + + index = 0; + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { + DownloadState downloadState = (*iter)->m_State; + if ((removeAll && (downloadState >= STATE_READY)) || + (removeState == downloadState)) { + removeFile(index, deleteFile); + delete *iter; + iter = m_ActiveDownloads.erase(iter); + } else { + ++iter; + ++index; + } + } + } else { + if (index >= m_ActiveDownloads.size()) { + reportError(tr("remove: invalid download index %1").arg(index)); + //emit update(-1); + endDisableDirWatcher(); + return; + } + + removeFile(index, deleteFile); + delete m_ActiveDownloads.at(index); + m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); + } + emit update(-1); + endDisableDirWatcher(); + } catch (const std::exception &e) { + qCritical("failed to remove download: %s", e.what()); + } + refreshList(); +} + + +void DownloadManager::cancelDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("cancel: invalid download index %1").arg(index)); + return; + } + + if (m_ActiveDownloads.at(index)->m_State == STATE_DOWNLOADING) { + setState(m_ActiveDownloads.at(index), STATE_CANCELING); + } +} + + +void DownloadManager::pauseDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("pause: invalid download index %1").arg(index)); + return; + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + + if (info->m_State == STATE_DOWNLOADING) { + if ((info->m_Reply != nullptr) && (info->m_Reply->isRunning())) { + setState(info, STATE_PAUSING); + } else { + setState(info, STATE_PAUSED); + } + } else if ((info->m_State == STATE_FETCHINGMODINFO) || (info->m_State == STATE_FETCHINGFILEINFO)) { + setState(info, STATE_READY); + } +} + +void DownloadManager::resumeDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("resume: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + info->m_Tries = AUTOMATIC_RETRIES; + resumeDownloadInt(index); +} + +void DownloadManager::resumeDownloadInt(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("resume (int): invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + // Check for finished download; + if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr + && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { + setState(info, STATE_DOWNLOADING); + downloadFinished(index); + return; + } + + if (info->isPausedState() || info->m_State == STATE_PAUSING) { + if (info->m_State == STATE_PAUSING) { + if (info->m_Output.isOpen()) { + writeData(info); + if (info->m_State == STATE_PAUSING) { + setState(info, STATE_PAUSED); + } + } + } + if ((info->m_Urls.size() == 0) + || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) { + emit showMessage(tr("No known download urls. Sorry, this download can't be resumed.")); + return; + } + if (info->m_State == STATE_ERROR) { + info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count(); + } + qDebug("request resume from url %s", qUtf8Printable(info->currentURL())); + QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit())); + request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); + if (info->m_State != STATE_ERROR) { + info->m_ResumePos = info->m_Output.size(); + QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; + request.setRawHeader("Range", rangeHeader); + } + std::get<0>(info->m_SpeedDiff) = 0; + std::get<1>(info->m_SpeedDiff) = 0; + std::get<2>(info->m_SpeedDiff) = 0; + std::get<3>(info->m_SpeedDiff) = 0; + std::get<4>(info->m_SpeedDiff) = 0; + qDebug("resume at %lld bytes", info->m_ResumePos); + startDownload(m_NexusInterface->getAccessManager()->get(request), info, true); + } + emit update(index); +} + + +DownloadManager::DownloadInfo *DownloadManager::downloadInfoByID(unsigned int id) +{ + auto iter = std::find_if(m_ActiveDownloads.begin(), m_ActiveDownloads.end(), + [id](DownloadInfo *info) { return info->m_DownloadID == id; }); + if (iter != m_ActiveDownloads.end()) { + return *iter; + } else { + return nullptr; + } +} + + +void DownloadManager::queryInfo(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("query: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + if (info->m_FileInfo->repository != "Nexus") { + qWarning("re-querying file info is currently only possible with Nexus"); + return; + } + + if (info->m_State < DownloadManager::STATE_READY) { + // UI shouldn't allow this + return; + } + + if (info->m_FileInfo->modID <= 0) { + QString fileName = getFileName(index); + QString ignore; + NexusInterface::interpretNexusFileName(fileName, ignore, info->m_FileInfo->modID, true); + if (info->m_FileInfo->modID < 0) { + bool ok = false; + int modId = QInputDialog::getInt( + nullptr, tr("Please enter the nexus mod id"), tr("Mod ID:"), 1, 1, + std::numeric_limits::max(), 1, &ok); + // careful now: while the dialog was displayed, events were processed. + // the download list might have changed and our info-ptr invalidated. + if (ok) + m_ActiveDownloads[index]->m_FileInfo->modID = modId; + return; + } + } + + if (info->m_FileInfo->gameName.size() == 0) { + SelectionDialog selection(tr("Please select the source game code for %1").arg(getFileName(index))); + + std::vector> choices = m_NexusInterface->getGameChoices(m_ManagedGame); + for (auto choice : choices) { + selection.addChoice(choice.first, choice.second, choice.first); + } + if (selection.exec() == QDialog::Accepted) { + info->m_FileInfo->gameName = selection.getChoiceData().toString(); + } else { + info->m_FileInfo->gameName = m_ManagedGame->gameShortName(); + } + } + info->m_ReQueried = true; + setState(info, STATE_FETCHINGMODINFO); +} + +void DownloadManager::visitOnNexus(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("VisitNexus: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + if (info->m_FileInfo->repository != "Nexus") { + qWarning("Visiting mod page is currently only possible with Nexus"); + return; + } + + if (info->m_State < DownloadManager::STATE_READY) { + // UI shouldn't allow this + return; + } + int modID = info->m_FileInfo->modID; + + QString gameName = info->m_FileInfo->gameName; + if (modID > 0) { + QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName))); + } + else { + emit showMessage(tr("Nexus ID for this Mod is unknown")); + } +} + +void DownloadManager::openFile(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("OpenFile: invalid download index %1").arg(index)); + return; + } + QDir path = QDir(m_OutputDirectory); + if (path.exists(getFileName(index))) { + + ::ShellExecuteW(nullptr, L"open", ToWString(QDir::toNativeSeparators(getFilePath(index))).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return; + } + + ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return; +} + +void DownloadManager::openInDownloadsFolder(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("OpenFileInDownloadsFolder: invalid download index %1").arg(index)); + return; + } + QString params = "/select,\""; + QDir path = QDir(m_OutputDirectory); + if (path.exists(getFileName(index))) { + params = params + QDir::toNativeSeparators(getFilePath(index)) + "\""; + + ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); + return; + } + else if (path.exists(getFileName(index) + ".unfinished")) { + params = params + QDir::toNativeSeparators(getFilePath(index)+".unfinished") + "\""; + + ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); + return; + } + + ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return; +} + + +int DownloadManager::numTotalDownloads() const +{ + return m_ActiveDownloads.size(); +} + +int DownloadManager::numPendingDownloads() const +{ + return m_PendingDownloads.size(); +} + +std::tuple DownloadManager::getPendingDownload(int index) +{ + if ((index < 0) || (index >= m_PendingDownloads.size())) { + throw MyException(tr("get pending: invalid download index %1").arg(index)); + } + + return m_PendingDownloads.at(index); +} + +QString DownloadManager::getFilePath(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("get path: invalid download index %1").arg(index)); + } + + return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName; +} + +QString DownloadManager::getFileTypeString(int fileType) +{ + switch (fileType) { + case 1: return tr("Main"); + case 2: return tr("Update"); + case 3: return tr("Optional"); + case 4: return tr("Old"); + case 5: return tr("Misc"); + default: return tr("Unknown"); + } +} + +QString DownloadManager::getDisplayName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("display name: invalid download index %1").arg(index)); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + + QTextDocument doc; + if (!info->m_FileInfo->name.isEmpty()) { + doc.setHtml(info->m_FileInfo->name); + return QString("%1 (%2, v%3)").arg(doc.toPlainText()) + .arg(getFileTypeString(info->m_FileInfo->fileCategory)) + .arg(info->m_FileInfo->version.displayString()); + } else { + doc.setHtml(info->m_FileName); + return doc.toPlainText(); + } +} + +QString DownloadManager::getFileName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file name: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_FileName; +} + +QDateTime DownloadManager::getFileTime(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file time: invalid download index %1").arg(index)); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + if (!info->m_Created.isValid()) { + info->m_Created = QFileInfo(info->m_Output).created(); + } + + return info->m_Created; +} + +qint64 DownloadManager::getFileSize(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file size: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_TotalSize; +} + + +std::pair DownloadManager::getProgress(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("progress: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_Progress; +} + + +DownloadManager::DownloadState DownloadManager::getState(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("state: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_State; +} + + +bool DownloadManager::isInfoIncomplete(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("infocomplete: invalid download index %1").arg(index)); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + if (info->m_FileInfo->repository != "Nexus") { + // other repositories currently don't support re-querying info anyway + return false; + } + return (info->m_FileInfo->fileID == 0) || (info->m_FileInfo->modID == 0); +} + + +int DownloadManager::getModID(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mod id: invalid download index %1").arg(index)); + } + return m_ActiveDownloads.at(index)->m_FileInfo->modID; +} + +QString DownloadManager::getGameName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mod id: invalid download index %1").arg(index)); + } + return m_ActiveDownloads.at(index)->m_FileInfo->gameName; +} + +bool DownloadManager::isHidden(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("ishidden: invalid download index %1").arg(index)); + } + return m_ActiveDownloads.at(index)->m_Hidden; +} + + +const ModRepositoryFileInfo *DownloadManager::getFileInfo(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file info: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_FileInfo; +} + + +void DownloadManager::markInstalled(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mark installed: invalid download index %1").arg(index)); + } + + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + DownloadInfo *info = m_ActiveDownloads.at(index); + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("installed", true); + metaFile.setValue("uninstalled", false); + + endDisableDirWatcher(); + + setState(m_ActiveDownloads.at(index), STATE_INSTALLED); +} + +void DownloadManager::markInstalled(QString fileName) +{ + int index = indexByName(fileName); + if (index >= 0) { + markInstalled(index); + } else { + DownloadInfo *info = getDownloadInfo(fileName); + if (info != nullptr) { + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("installed", true); + metaFile.setValue("uninstalled", false); + delete info; + + endDisableDirWatcher(); + } + } +} + +DownloadManager::DownloadInfo* DownloadManager::getDownloadInfo(QString fileName) +{ + return DownloadInfo::createFromMeta(fileName, true, m_OutputDirectory); +} + +void DownloadManager::markUninstalled(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mark uninstalled: invalid download index %1").arg(index)); + } + + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + DownloadInfo *info = m_ActiveDownloads.at(index); + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("uninstalled", true); + + endDisableDirWatcher(); + + setState(m_ActiveDownloads.at(index), STATE_UNINSTALLED); +} + + +void DownloadManager::markUninstalled(QString fileName) +{ + int index = indexByName(fileName); + if (index >= 0) { + markUninstalled(index); + } else { + QString filePath = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + fileName; + DownloadInfo *info = getDownloadInfo(filePath); + if (info != nullptr) { + + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("uninstalled", true); + delete info; + + endDisableDirWatcher(); + } + } +} + + +QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const +{ + QString fullPath = m_OutputDirectory + "/" + baseName; + if (QFile::exists(fullPath) && rename) { + int i = 1; + while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) { + ++i; + } + + fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName); + } + return fullPath; +} + + +QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply) +{ + if (reply->hasRawHeader("Content-Disposition")) { + std::regex exp("filename=\"(.*)\""); + + std::cmatch result; + if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) { + return QString::fromUtf8(result.str(1).c_str()); + } + } + + return QString(); +} + + +void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadManager::DownloadState state) +{ + int row = 0; + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i] == info) { + row = i; + break; + } + } + info->m_State = state; + switch (state) { + case STATE_PAUSED: + case STATE_ERROR: { + info->m_Reply->abort(); + info->m_Output.close(); + } break; + case STATE_CANCELED: { + info->m_Reply->abort(); + } break; + case STATE_FETCHINGMODINFO: { + m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); + } break; + case STATE_FETCHINGFILEINFO: { + m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); + } break; + case STATE_READY: { + createMetaFile(info); + emit downloadComplete(row); + } break; + default: /* NOP */ break; + } + emit stateChanged(row, state); +} + + +DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const +{ + // reverse search as newer, thus more relevant, downloads are at the end + for (int i = m_ActiveDownloads.size() - 1; i >= 0; --i) { + if (m_ActiveDownloads[i]->m_Reply == reply) { + if (index != nullptr) { + *index = i; + } + return m_ActiveDownloads[i]; + } + } + return nullptr; +} + + +void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + if (bytesTotal == 0) { + return; + } + int index = 0; + try { + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != nullptr) { + info->m_HasData = true; + if (info->m_State == STATE_CANCELING) { + setState(info, STATE_CANCELED); + } else if (info->m_State == STATE_PAUSING) { + setState(info, STATE_PAUSED); + } + else { + if (bytesTotal > info->m_TotalSize) { + info->m_TotalSize = bytesTotal; + } + int oldProgress = info->m_Progress.first; + info->m_Progress.first = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); + + int elapsed = info->m_StartTime.elapsed(); + std::get<0>(info->m_SpeedDiff) = bytesReceived - std::get<2>(info->m_SpeedDiff); + std::get<1>(info->m_SpeedDiff) = elapsed - std::get<3>(info->m_SpeedDiff); + std::get<2>(info->m_SpeedDiff) = bytesReceived; + std::get<3>(info->m_SpeedDiff) = elapsed; + + double calc = ((double)std::get<0>(info->m_SpeedDiff)) / (((double)(std::get<1>(info->m_SpeedDiff)) / 5000.0)); + std::get<4>(info->m_SpeedDiff) = ((calc*0.5) + (std::get<4>(info->m_SpeedDiff)*1.5)) / 2; + + // calculate the download speed + double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000); + + QString unit; + if (speed < 1000) { + unit = "B/s"; + } + else if (speed < 1000*1024) { + speed /= 1024; + unit = "KB/s"; + } + else { + speed /= 1024 * 1024; + unit = "MB/s"; + } + + info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(QString::number(speed, 'f', 1)).arg(unit); + + TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal); + emit update(index); + } + } + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in processing progress event).")); + } +} + + +void DownloadManager::downloadReadyRead() +{ + try { + writeData(findDownload(this->sender())); + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in processing downloaded data).")); + } +} + + +void DownloadManager::createMetaFile(DownloadInfo *info) +{ + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat); + metaFile.setValue("gameName", info->m_FileInfo->gameName); + metaFile.setValue("modID", info->m_FileInfo->modID); + metaFile.setValue("fileID", info->m_FileInfo->fileID); + metaFile.setValue("url", info->m_Urls.join(";")); + metaFile.setValue("name", info->m_FileInfo->name); + metaFile.setValue("description", info->m_FileInfo->description); + metaFile.setValue("modName", info->m_FileInfo->modName); + metaFile.setValue("version", info->m_FileInfo->version.canonicalString()); + metaFile.setValue("newestVersion", info->m_FileInfo->newestVersion.canonicalString()); + metaFile.setValue("fileTime", info->m_FileInfo->fileTime); + metaFile.setValue("fileCategory", info->m_FileInfo->fileCategory); + metaFile.setValue("category", info->m_FileInfo->categoryID); + metaFile.setValue("repository", info->m_FileInfo->repository); + metaFile.setValue("userData", info->m_FileInfo->userData); + metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED); + metaFile.setValue("uninstalled", info->m_State == DownloadManager::STATE_UNINSTALLED); + metaFile.setValue("paused", (info->m_State == DownloadManager::STATE_PAUSED) || + (info->m_State == DownloadManager::STATE_ERROR)); + metaFile.setValue("removed", info->m_Hidden); + + endDisableDirWatcher(); + // slightly hackish... + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i] == info) { + emit update(i); + } + } +} + + +void DownloadManager::nxmDescriptionAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + QVariantMap result = resultData.toMap(); + + DownloadInfo *info = downloadInfoByID(userData.toInt()); + if (info == nullptr) return; + info->m_FileInfo->categoryID = result["category_id"].toInt(); + QTextDocument doc; + doc.setHtml(result["name"].toString().trimmed()); + info->m_FileInfo->modName = doc.toPlainText(); + info->m_FileInfo->newestVersion.parse(result["version"].toString()); + if (info->m_FileInfo->fileID != 0) { + setState(info, STATE_READY); + } else { + setState(info, STATE_FETCHINGFILEINFO); + } +} + +void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + DownloadInfo *info = downloadInfoByID(userData.toInt()); + if (info == nullptr) return; + + QVariantMap result = resultData.toMap(); + QVariantList files = result["files"].toList(); + + + // MO sometimes prepends _ to the filename in case of duplicate downloads. + // this may muck up the file name comparison + QString alternativeLocalName = info->m_FileName; + + QRegExp expression("^\\d_(.*)$"); + if (expression.indexIn(alternativeLocalName) == 0) { + alternativeLocalName = expression.cap(1); + } + + bool found = false; + + for (QVariant file : files) { + QVariantMap fileInfo = file.toMap(); + QString fileName = fileInfo["file_name"].toString(); + QString fileNameVariant = fileName.mid(0).replace(' ', '_'); + if ((fileName == info->m_FileName) || (fileName == alternativeLocalName) || + (fileNameVariant == info->m_FileName) || (fileNameVariant == alternativeLocalName)) { + info->m_FileInfo->name = fileInfo["name"].toString(); + info->m_FileInfo->version.parse(fileInfo["version"].toString()); + if (!info->m_FileInfo->version.isValid()) { + info->m_FileInfo->version = info->m_FileInfo->newestVersion; + } + info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); + info->m_FileInfo->fileTime = QDateTime::fromMSecsSinceEpoch(fileInfo["uploaded_timestamp"].toLongLong()); + info->m_FileInfo->fileID = fileInfo["file_id"].toInt(); + info->m_FileInfo->fileName = fileInfo["file_name"].toString(); + info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["changelog_html"].toString()); + found = true; + break; + } + } + + if (info->m_ReQueried) { + if (found) { + emit showMessage(tr("Information updated")); + } else if (result.count() == 0) { + emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); + } else { + SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one.")); + for (QVariant file : result) { + QVariantMap fileInfo = file.toMap(); + selection.addChoice(fileInfo["file_name"].toString(), "", file); + } + if (selection.exec() == QDialog::Accepted) { + QVariantMap fileInfo = selection.getChoiceData().toMap(); + info->m_FileInfo->name = fileInfo["name"].toString(); + info->m_FileInfo->version.parse(fileInfo["version"].toString()); + info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); + info->m_FileInfo->fileID = fileInfo["file_id"].toInt(); + } else { + emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); + } + } + } else { + if (info->m_FileInfo->fileID == 0) { + qWarning("could not determine file id for %s (state %d)", + qUtf8Printable(info->m_FileName), info->m_State); + } + } + + setState(info, STATE_READY); +} + + +void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + ModRepositoryFileInfo *info = new ModRepositoryFileInfo(); + + QVariantMap result = resultData.toMap(); + info->name = result["name"].toString(); + info->version.parse(result["version"].toString()); + if (!info->version.isValid()) { + info->version = info->newestVersion; + } + info->fileName = result["file_name"].toString(); + info->fileCategory = result["category_id"].toInt(); + info->fileTime = QDateTime::fromMSecsSinceEpoch(result["uploaded_timestamp"].toLongLong()); + info->description = BBCode::convertToHTML(result["changelog_html"].toString()); + + info->repository = "Nexus"; + info->gameName = gameName; + info->modID = modID; + info->fileID = fileID; + + QObject *test = info; + m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(gameName, modID, fileID, this, qVariantFromValue(test), QString())); +} + +static int evaluateFileInfoMap(const QVariantMap &map, const std::map &preferredServers) +{ + int result = 0; + + auto preference = preferredServers.find(map["name"].toString()); + + if (preference != preferredServers.end()) { + result += 100 + preference->second * 20; + } + + return result; +} + +// sort function to sort by best download server +bool DownloadManager::ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS) +{ + return evaluateFileInfoMap(LHS.toMap(), preferredServers) > evaluateFileInfoMap(RHS.toMap(), preferredServers); +} + +int DownloadManager::startDownloadURLs(const QStringList &urls) +{ + ModRepositoryFileInfo info; + addDownload(urls, "", -1, -1, &info); + return m_ActiveDownloads.size() - 1; +} + +int DownloadManager::startDownloadNexusFile(int modID, int fileID) +{ + int newID = m_ActiveDownloads.size(); + addNXMDownload(QString("nxm://%1/mods/%2/files/%3").arg(m_ManagedGame->gameShortName()).arg(modID).arg(fileID)); + return newID; +} + +QString DownloadManager::downloadPath(int id) +{ + return getFilePath(id); +} + +int DownloadManager::indexByName(const QString &fileName) const +{ + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i]->m_FileName == fileName) { + return i; + } + } + return -1; +} + +void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + ModRepositoryFileInfo *info = qobject_cast(qvariant_cast(userData)); + QVariantList resultList = resultData.toList(); + if (resultList.length() == 0) { + removePending(gameName, modID, fileID); + emit showMessage(tr("No download server available. Please try again later.")); + return; + } + + std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2)); + + info->userData["downloadMap"] = resultList; + + QStringList URLs; + + foreach (const QVariant &server, resultList) { + URLs.append(server.toMap()["URI"].toString()); + } + addDownload(URLs, gameName, modID, fileID, info); +} + + +void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + int index = 0; + + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { + DownloadInfo *info = *iter; + if (info->m_FileInfo->modID == modID) { + if (info->m_State < STATE_FETCHINGMODINFO) { + m_ActiveDownloads.erase(iter); + delete info; + } else { + setState(info, STATE_READY); + } + emit update(index); + break; + } + } + + removePending(gameName, modID, fileID); + emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString)); +} + + +void DownloadManager::downloadFinished(int index) +{ + DownloadInfo *info; + if (index) + info = m_ActiveDownloads[index]; + else + info = findDownload(this->sender(), &index); + + if (info != nullptr) { + QNetworkReply *reply = info->m_Reply; + QByteArray data; + if (reply->isOpen() && info->m_HasData) { + data = reply->readAll(); + info->m_Output.write(data); + } + info->m_Output.close(); + TaskProgressManager::instance().forgetMe(info->m_TaskProgressId); + + bool error = false; + if ((info->m_State != STATE_CANCELING) && + (info->m_State != STATE_PAUSING)) { + bool textData = reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive); + if (textData) + emit showMessage(tr("Warning: Content type is: %1").arg(reply->header(QNetworkRequest::ContentTypeHeader).toString())); + if ((info->m_Output.size() == 0) || + ((reply->error() != QNetworkReply::NoError) + && (reply->error() != QNetworkReply::OperationCanceledError))) { + if (reply->error() == QNetworkReply::UnknownContentError) + emit showMessage(tr("Download header content length: %1 downloaded file size: %2").arg(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()).arg(info->m_Output.size())); + if (info->m_Tries == 0) { + emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); + } + error = true; + setState(info, STATE_ERROR); + } + } + + if (info->m_State == STATE_CANCELING) { + setState(info, STATE_CANCELED); + } else if (info->m_State == STATE_PAUSING) { + if (info->m_Output.isOpen() && info->m_HasData) { + info->m_Output.write(info->m_Reply->readAll()); + } + setState(info, STATE_PAUSED); + } + + if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) { + emit aboutToUpdate(); + info->m_Output.remove(); + delete info; + m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); + if (error) + emit showMessage(tr("We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.")); + emit update(-1); + } else if (info->isPausedState() || info->m_State == STATE_PAUSING) { + info->m_Output.close(); + createMetaFile(info); + emit update(index); + } else { + QString url = info->m_Urls[info->m_CurrentUrl]; + if (info->m_FileInfo->userData.contains("downloadMap")) { + foreach (const QVariant &server, info->m_FileInfo->userData["downloadMap"].toList()) { + QVariantMap serverMap = server.toMap(); + if (serverMap["URI"].toString() == url) { + int deltaTime = info->m_StartTime.secsTo(QTime::currentTime()); + if (deltaTime > 5) { + emit downloadSpeed(serverMap["Name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime); + } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise + break; + } + } + } + + bool isNexus = info->m_FileInfo->repository == "Nexus"; + // need to change state before changing the file name, otherwise .unfinished is appended + if (isNexus) { + setState(info, STATE_FETCHINGMODINFO); + } else { + setState(info, STATE_NOFETCH); + } + + QString newName = getFileNameFromNetworkReply(reply); + QString oldName = QFileInfo(info->m_Output).fileName(); + + startDisableDirWatcher(); + if (!newName.isEmpty() && (oldName.isEmpty())) { + info->setName(getDownloadFileName(newName), true); + } else { + info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension + } + endDisableDirWatcher(); + + if (!isNexus) { + setState(info, STATE_READY); + } + + emit update(index); + } + reply->close(); + reply->deleteLater(); + + if ((info->m_Tries > 0) && error) { + --info->m_Tries; + resumeDownloadInt(index); + } + } else { + qWarning("no download index %d", index); + } +} + + +void DownloadManager::downloadError(QNetworkReply::NetworkError error) +{ + if (error != QNetworkReply::OperationCanceledError) { + QNetworkReply *reply = qobject_cast(sender()); + qWarning("%s (%d)", reply != nullptr ? qUtf8Printable(reply->errorString()) + : "Download error occured", + error); + } +} + + +void DownloadManager::metaDataChanged() +{ + int index = 0; + + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != nullptr) { + QString newName = getFileNameFromNetworkReply(info->m_Reply); + if (!newName.isEmpty() && (info->m_FileName.isEmpty())) { + startDisableDirWatcher(); + info->setName(getDownloadFileName(newName), true); + endDisableDirWatcher(); + refreshAlphabeticalTranslation(); + if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { + reportError(tr("failed to re-open %1").arg(info->m_FileName)); + setState(info, STATE_CANCELING); + } + } + } else { + qWarning("meta data event for unknown download"); + } +} + +void DownloadManager::directoryChanged(const QString&) +{ + if(DownloadManager::m_DirWatcherDisabler==0) + refreshList(); +} + +void DownloadManager::managedGameChanged(MOBase::IPluginGame const *managedGame) +{ + m_ManagedGame = managedGame; +} + +void DownloadManager::checkDownloadTimeout() +{ + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i]->m_StartTime.elapsed() - std::get<3>(m_ActiveDownloads[i]->m_SpeedDiff) > 5 * 1000 && + m_ActiveDownloads[i]->m_State == STATE_DOWNLOADING && m_ActiveDownloads[i]->m_Reply != nullptr && + m_ActiveDownloads[i]->m_Reply->isOpen()) { + pauseDownload(i); + downloadFinished(i); + resumeDownload(i); + } + } +} + +void DownloadManager::writeData(DownloadInfo *info) +{ + if (info != nullptr) { + qint64 ret = info->m_Output.write(info->m_Reply->readAll()); + if (ret < info->m_Reply->size()) { + QString fileName = info->m_FileName; // m_FileName may be destroyed after setState + setState(info, DownloadState::STATE_CANCELED); + qCritical(QString("Unable to write download \"%2\" to drive (return %1)").arg(ret).arg(info->m_FileName).toLocal8Bit()); + reportError(tr("Unable to write download to drive (return %1).\n" + "Check the drive's available storage.\n\n" + "Canceling download \"%2\"...").arg(ret).arg(fileName)); + } + } +} diff --git a/src/downloadmanager.h b/src/downloadmanager.h index 4cbe31b7..3c36143e 100644 --- a/src/downloadmanager.h +++ b/src/downloadmanager.h @@ -1,568 +1,564 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef DOWNLOADMANAGER_H -#define DOWNLOADMANAGER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace MOBase { class IPluginGame; } - -class NexusInterface; -class PluginContainer; - -/*! - * \brief manages downloading of files and provides progress information for gui elements - **/ -class DownloadManager : public MOBase::IDownloadManager -{ - Q_OBJECT - -public: - - enum DownloadState { - STATE_STARTED = 0, - STATE_DOWNLOADING, - STATE_CANCELING, - STATE_PAUSING, - STATE_CANCELED, - STATE_PAUSED, - STATE_ERROR, - STATE_FETCHINGMODINFO, - STATE_FETCHINGFILEINFO, - STATE_NOFETCH, - STATE_READY, - STATE_INSTALLED, - STATE_UNINSTALLED - }; - -private: - - struct DownloadInfo { - ~DownloadInfo() { delete m_FileInfo; } - unsigned int m_DownloadID; - QString m_FileName; - QFile m_Output; - QNetworkReply *m_Reply; - QTime m_StartTime; - qint64 m_PreResumeSize; - std::pair m_Progress; - std::tuple m_SpeedDiff; - bool m_HasData; - DownloadState m_State; - int m_CurrentUrl; - QStringList m_Urls; - qint64 m_ResumePos; - qint64 m_TotalSize; - QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere - - int m_Tries; - bool m_ReQueried; - - quint32 m_TaskProgressId; - - MOBase::ModRepositoryFileInfo *m_FileInfo { nullptr }; - - bool m_Hidden; - - static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs); - static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory); - - /** - * @brief rename the file - * this will change the file name as well as the display name. It will automatically - * append .unfinished to the name if this file is still being downloaded - * @param newName the new name to setName - * @param renameFile if true, the file is assumed to exist and renamed. If the file does not - * yet exist, set this to false - **/ - void setName(QString newName, bool renameFile); - - unsigned int downloadID() { return m_DownloadID; } - - bool isPausedState(); - - QString currentURL(); - private: - static unsigned int s_NextDownloadID; - private: - DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple(0,0,0,0,0)), m_HasData(false) {} - }; - -public: - - /** - * @brief constructor - * - * @param nexusInterface interface to use to retrieve information from the relevant nexus page - * @param parent parent object - **/ - explicit DownloadManager(NexusInterface *nexusInterface, QObject *parent); - - ~DownloadManager(); - - /** - * @brief determine if a download is currently in progress - * - * @return true if there is currently a download in progress - **/ - bool downloadsInProgress(); - - /** - * @brief determine if a download is currently in progress, does not count paused ones. - * - * @return true if there is currently a download in progress (that is not paused already). - **/ - bool downloadsInProgressNoPause(); - - /** - * @brief set the output directory to write to - * - * @param outputDirectory the new output directory - **/ - void setOutputDirectory(const QString &outputDirectory); - - /** - * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called - * - **/ - static void startDisableDirWatcher(); - - /** - * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called - **/ - static void endDisableDirWatcher(); - - /** - * @return current download directory - **/ - QString getOutputDirectory() const { return m_OutputDirectory; } - - /** - * @brief setPreferredServers set the list of preferred servers - */ - void setPreferredServers(const std::map &preferredServers); - - /** - * @brief set the list of supported extensions - * @param extensions list of supported extensions - */ - void setSupportedExtensions(const QStringList &extensions); - - /** - * @brief sets whether hidden files are to be shown after all - */ - void setShowHidden(bool showHidden); - - void setPluginContainer(PluginContainer *pluginContainer); - - /** - * @brief download from an already open network connection - * - * @param reply the network reply to download from - * @param fileInfo information about the file, like mod id, file id, version, ... - * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again - **/ - bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo); - - /** - * @brief download from an already open network connection - * - * @param reply the network reply to download from - * @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name - * @param fileInfo information previously retrieved from the nexus network - * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again - **/ - bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo()); - - /** - * @brief start a download using a nxm-link - * - * starts a download using a nxm-link. The download manager will first query the nexus - * page for file information. - * @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711 - * @todo the game name encoded into the link is currently ignored, all downloads are incorrectly assumed to be for the identified game - **/ - void addNXMDownload(const QString &url); - - /** - * @brief retrieve the total number of downloads, both finished and unfinished including downloads from previous sessions - * - * @return total number of downloads - **/ - int numTotalDownloads() const; - - /** - * @brief retrieve number of pending downloads (nexus downloads for which we don't know the name and url yet) - * @return number of pending downloads - */ - int numPendingDownloads() const; - - /** - * @brief retrieve the info of a pending download - * @param index index of the pending download (index in the range [0, numPendingDownloads()[) - * @return pair of modid, fileid - */ - std::tuple getPendingDownload(int index); - - /** - * @brief retrieve the full path to the download specified by index - * - * @param index the index to look up - * @return absolute path of the file - **/ - QString getFilePath(int index) const; - - /** - * @brief retrieve a descriptive name of the download specified by index - * - * @param index index of the file to look up - * @return display name of the file - **/ - QString getDisplayName(int index) const; - - /** - * @brief retrieve the filename of the download specified by index - * - * @param index index of the file to look up - * @return name of the file - **/ - QString getFileName(int index) const; - - /** - * @brief retrieve the file size of the download specified by index - * - * @param index index of the file to look up - * @return size of the file (total size during download) - */ - qint64 getFileSize(int index) const; - - /** - * @brief retrieve the creation time of the download specified by index - * @param index index of the file to look up - * @return size of the file (total size during download) - */ - QDateTime getFileTime(int index) const; - - /** - * @brief retrieve the current progress of the download specified by index - * - * @param index index of the file to look up - * @return progress of the download in percent (integer) - **/ - std::pair getProgress(int index) const; - - /** - * @brief retrieve the current state of the download - * - * retrieve the current state of the download. A download usually goes through - * the following states: - * started -> downloading -> fetching mod info -> fetching file info -> done - * in case of downloads started via nxm-link, file information is fetched first - * - * @param index index of the file to look up - * @return the download state - **/ - DownloadState getState(int index) const; - - /** - * @param index index of the file to look up - * @return true if the nexus information for this download is not complete - **/ - bool isInfoIncomplete(int index) const; - - /** - * @brief retrieve the nexus mod id of the download specified by index - * - * @param index index of the file to look up - * @return the nexus mod id - **/ - int getModID(int index) const; - - /** - * @brief retrieve the game name of the downlaod specified by the index - * - * @param index index of the file to look up - * @return the game name - **/ - QString getGameName(int index) const; - - /** - * @brief determine if the specified file is supposed to be hidden - * @param index index of the file to look up - * @return true if the specified file is supposed to be hidden - */ - bool isHidden(int index) const; - - /** - * @brief retrieve all nexus info of the download specified by index - * - * @param index index of the file to look up - * @return the nexus mod information - **/ - const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const; - - /** - * @brief mark a download as installed - * - * @param index index of the file to mark installed - */ - void markInstalled(int index); - - void markInstalled(QString download); - - /** - * @brief mark a download as uninstalled - * - * @param index index of the file to mark uninstalled - */ - void markUninstalled(int index); - - void markUninstalled(QString download); - - /** - * @brief refreshes the list of downloads - */ - void refreshList(); - - /** - * @brief Sort function for download servers - * @param LHS - * @param RHS - * @return - */ - static bool ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS); - - - virtual int startDownloadURLs(const QStringList &urls); - - virtual int startDownloadNexusFile(int modID, int fileID); - - virtual QString downloadPath(int id); - - /** - * @brief retrieve a download index from the filename - * @param fileName file to look up - * @return index of that download or -1 if it wasn't found - */ - int indexByName(const QString &fileName) const; - - void pauseAll(); - -signals: - - void aboutToUpdate(); - - /** - * @brief signals that the specified download has changed - * - * @param row the row that changed. This corresponds to the download index - **/ - void update(int row); - - /** - * @brief signals the ui that a message should be displayed - * - * @param message the message to display - **/ - void showMessage(const QString &message); - - /** - * @brief emitted whenever the state of a download changes - * @param row the row that changed - * @param state the new state - */ - void stateChanged(int row, DownloadManager::DownloadState state); - - /** - * @brief emitted whenever a download completes successfully, reporting the download speed for the server used - */ - void downloadSpeed(const QString &serverName, int bytesPerSecond); - - /** - * @brief emitted whenever a new download is added to the list - */ - void downloadAdded(); - -public slots: - - /** - * @brief removes the specified download - * - * @param index index of the download to remove - * @param deleteFile if true, the file will also be deleted from disc, otherwise it is only marked as hidden. - **/ - void removeDownload(int index, bool deleteFile); - - /** - * @brief restores the specified download to view (which was previously hidden - * @param index index of the download to restore - */ - void restoreDownload(int index); - - /** - * @brief cancel the specified download. This will lead to the corresponding file to be deleted - * - * @param index index of the download to cancel - **/ - void cancelDownload(int index); - - void pauseDownload(int index); - - void resumeDownload(int index); - - void queryInfo(int index); - - void visitOnNexus(int index); - - void openFile(int index); - - void openInDownloadsFolder(int index); - - void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - - void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - - void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - - void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - - void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString); - - void managedGameChanged(MOBase::IPluginGame const *gamePlugin); - -private slots: - - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); - void downloadReadyRead(); - void downloadFinished(int index = 0); - void downloadError(QNetworkReply::NetworkError error); - void metaDataChanged(); - void directoryChanged(const QString &dirctory); - void checkDownloadTimeout(); - -private: - - void createMetaFile(DownloadInfo *info); - DownloadManager::DownloadInfo* getDownloadInfo(QString fileName); - -public: - - /** Get a unique filename for a download. - * - * This allows you multiple versions of download files, useful if the file - * comes from a web site with no version control - * - * @param basename: Name of the file - * - * @return Unique(ish) name - */ - QString getDownloadFileName(const QString &baseName, bool rename = false) const; - -private: - - void startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume); - void resumeDownloadInt(int index); - - /** - * @brief start a download from a url - * - * @param url the url to download from - * @param fileInfo information previously retrieved from the mod page - * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again - **/ - bool addDownload(const QStringList &URLs, QString gameName, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo); - - // important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time - DownloadInfo *findDownload(QObject *reply, int *index = nullptr) const; - - void removeFile(int index, bool deleteFile); - - void refreshAlphabeticalTranslation(); - - bool ByName(int LHS, int RHS); - - QString getFileNameFromNetworkReply(QNetworkReply *reply); - - void setState(DownloadInfo *info, DownloadManager::DownloadState state); - - DownloadInfo *downloadInfoByID(unsigned int id); - - QDateTime matchDate(const QString &timeString); - - void removePending(QString gameName, int modID, int fileID); - - static QString getFileTypeString(int fileType); - - void writeData(DownloadInfo *info); - -private: - - static const int AUTOMATIC_RETRIES = 3; - -private: - - NexusInterface *m_NexusInterface; - - QVector> m_PendingDownloads; - - QVector m_ActiveDownloads; - - QString m_OutputDirectory; - std::map m_PreferredServers; - QStringList m_SupportedExtensions; - std::set m_RequestIDs; - QVector m_AlphabeticalTranslation; - - QFileSystemWatcher m_DirWatcher; - - //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files - //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder. - //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui. - static int m_DirWatcherDisabler; - - - std::map m_DownloadFails; - - bool m_ShowHidden; - - QRegExp m_DateExpression; - - MOBase::IPluginGame const *m_ManagedGame; - - QTimer m_TimeoutTimer; -}; - - - -#endif // DOWNLOADMANAGER_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef DOWNLOADMANAGER_H +#define DOWNLOADMANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase { class IPluginGame; } + +class NexusInterface; +class PluginContainer; + +/*! + * \brief manages downloading of files and provides progress information for gui elements + **/ +class DownloadManager : public MOBase::IDownloadManager +{ + Q_OBJECT + +public: + + enum DownloadState { + STATE_STARTED = 0, + STATE_DOWNLOADING, + STATE_CANCELING, + STATE_PAUSING, + STATE_CANCELED, + STATE_PAUSED, + STATE_ERROR, + STATE_FETCHINGMODINFO, + STATE_FETCHINGFILEINFO, + STATE_NOFETCH, + STATE_READY, + STATE_INSTALLED, + STATE_UNINSTALLED + }; + +private: + + struct DownloadInfo { + ~DownloadInfo() { delete m_FileInfo; } + unsigned int m_DownloadID; + QString m_FileName; + QFile m_Output; + QNetworkReply *m_Reply; + QTime m_StartTime; + qint64 m_PreResumeSize; + std::pair m_Progress; + std::tuple m_SpeedDiff; + bool m_HasData; + DownloadState m_State; + int m_CurrentUrl; + QStringList m_Urls; + qint64 m_ResumePos; + qint64 m_TotalSize; + QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere + + int m_Tries; + bool m_ReQueried; + + quint32 m_TaskProgressId; + + MOBase::ModRepositoryFileInfo *m_FileInfo { nullptr }; + + bool m_Hidden; + + static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs); + static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory); + + /** + * @brief rename the file + * this will change the file name as well as the display name. It will automatically + * append .unfinished to the name if this file is still being downloaded + * @param newName the new name to setName + * @param renameFile if true, the file is assumed to exist and renamed. If the file does not + * yet exist, set this to false + **/ + void setName(QString newName, bool renameFile); + + unsigned int downloadID() { return m_DownloadID; } + + bool isPausedState(); + + QString currentURL(); + private: + static unsigned int s_NextDownloadID; + private: + DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple(0,0,0,0,0)), m_HasData(false) {} + }; + +public: + + /** + * @brief constructor + * + * @param nexusInterface interface to use to retrieve information from the relevant nexus page + * @param parent parent object + **/ + explicit DownloadManager(NexusInterface *nexusInterface, QObject *parent); + + ~DownloadManager(); + + /** + * @brief determine if a download is currently in progress + * + * @return true if there is currently a download in progress + **/ + bool downloadsInProgress(); + + /** + * @brief determine if a download is currently in progress, does not count paused ones. + * + * @return true if there is currently a download in progress (that is not paused already). + **/ + bool downloadsInProgressNoPause(); + + /** + * @brief set the output directory to write to + * + * @param outputDirectory the new output directory + **/ + void setOutputDirectory(const QString &outputDirectory); + + /** + * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called + * + **/ + static void startDisableDirWatcher(); + + /** + * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called + **/ + static void endDisableDirWatcher(); + + /** + * @return current download directory + **/ + QString getOutputDirectory() const { return m_OutputDirectory; } + + /** + * @brief setPreferredServers set the list of preferred servers + */ + void setPreferredServers(const std::map &preferredServers); + + /** + * @brief set the list of supported extensions + * @param extensions list of supported extensions + */ + void setSupportedExtensions(const QStringList &extensions); + + /** + * @brief sets whether hidden files are to be shown after all + */ + void setShowHidden(bool showHidden); + + void setPluginContainer(PluginContainer *pluginContainer); + + /** + * @brief download from an already open network connection + * + * @param reply the network reply to download from + * @param fileInfo information about the file, like mod id, file id, version, ... + * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again + **/ + bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo); + + /** + * @brief download from an already open network connection + * + * @param reply the network reply to download from + * @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name + * @param fileInfo information previously retrieved from the nexus network + * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again + **/ + bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo()); + + /** + * @brief start a download using a nxm-link + * + * starts a download using a nxm-link. The download manager will first query the nexus + * page for file information. + * @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711 + * @todo the game name encoded into the link is currently ignored, all downloads are incorrectly assumed to be for the identified game + **/ + void addNXMDownload(const QString &url); + + /** + * @brief retrieve the total number of downloads, both finished and unfinished including downloads from previous sessions + * + * @return total number of downloads + **/ + int numTotalDownloads() const; + + /** + * @brief retrieve number of pending downloads (nexus downloads for which we don't know the name and url yet) + * @return number of pending downloads + */ + int numPendingDownloads() const; + + /** + * @brief retrieve the info of a pending download + * @param index index of the pending download (index in the range [0, numPendingDownloads()[) + * @return pair of modid, fileid + */ + std::tuple getPendingDownload(int index); + + /** + * @brief retrieve the full path to the download specified by index + * + * @param index the index to look up + * @return absolute path of the file + **/ + QString getFilePath(int index) const; + + /** + * @brief retrieve a descriptive name of the download specified by index + * + * @param index index of the file to look up + * @return display name of the file + **/ + QString getDisplayName(int index) const; + + /** + * @brief retrieve the filename of the download specified by index + * + * @param index index of the file to look up + * @return name of the file + **/ + QString getFileName(int index) const; + + /** + * @brief retrieve the file size of the download specified by index + * + * @param index index of the file to look up + * @return size of the file (total size during download) + */ + qint64 getFileSize(int index) const; + + /** + * @brief retrieve the creation time of the download specified by index + * @param index index of the file to look up + * @return size of the file (total size during download) + */ + QDateTime getFileTime(int index) const; + + /** + * @brief retrieve the current progress of the download specified by index + * + * @param index index of the file to look up + * @return progress of the download in percent (integer) + **/ + std::pair getProgress(int index) const; + + /** + * @brief retrieve the current state of the download + * + * retrieve the current state of the download. A download usually goes through + * the following states: + * started -> downloading -> fetching mod info -> fetching file info -> done + * in case of downloads started via nxm-link, file information is fetched first + * + * @param index index of the file to look up + * @return the download state + **/ + DownloadState getState(int index) const; + + /** + * @param index index of the file to look up + * @return true if the nexus information for this download is not complete + **/ + bool isInfoIncomplete(int index) const; + + /** + * @brief retrieve the nexus mod id of the download specified by index + * + * @param index index of the file to look up + * @return the nexus mod id + **/ + int getModID(int index) const; + + /** + * @brief retrieve the game name of the downlaod specified by the index + * + * @param index index of the file to look up + * @return the game name + **/ + QString getGameName(int index) const; + + /** + * @brief determine if the specified file is supposed to be hidden + * @param index index of the file to look up + * @return true if the specified file is supposed to be hidden + */ + bool isHidden(int index) const; + + /** + * @brief retrieve all nexus info of the download specified by index + * + * @param index index of the file to look up + * @return the nexus mod information + **/ + const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const; + + /** + * @brief mark a download as installed + * + * @param index index of the file to mark installed + */ + void markInstalled(int index); + + void markInstalled(QString download); + + /** + * @brief mark a download as uninstalled + * + * @param index index of the file to mark uninstalled + */ + void markUninstalled(int index); + + void markUninstalled(QString download); + + /** + * @brief refreshes the list of downloads + */ + void refreshList(); + + /** + * @brief Sort function for download servers + * @param LHS + * @param RHS + * @return + */ + static bool ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS); + + + virtual int startDownloadURLs(const QStringList &urls); + + virtual int startDownloadNexusFile(int modID, int fileID); + + virtual QString downloadPath(int id); + + /** + * @brief retrieve a download index from the filename + * @param fileName file to look up + * @return index of that download or -1 if it wasn't found + */ + int indexByName(const QString &fileName) const; + + void pauseAll(); + +signals: + + void aboutToUpdate(); + + /** + * @brief signals that the specified download has changed + * + * @param row the row that changed. This corresponds to the download index + **/ + void update(int row); + + /** + * @brief signals the ui that a message should be displayed + * + * @param message the message to display + **/ + void showMessage(const QString &message); + + /** + * @brief emitted whenever the state of a download changes + * @param row the row that changed + * @param state the new state + */ + void stateChanged(int row, DownloadManager::DownloadState state); + + /** + * @brief emitted whenever a download completes successfully, reporting the download speed for the server used + */ + void downloadSpeed(const QString &serverName, int bytesPerSecond); + + /** + * @brief emitted whenever a new download is added to the list + */ + void downloadAdded(); + +public slots: + + /** + * @brief removes the specified download + * + * @param index index of the download to remove + * @param deleteFile if true, the file will also be deleted from disc, otherwise it is only marked as hidden. + **/ + void removeDownload(int index, bool deleteFile); + + /** + * @brief restores the specified download to view (which was previously hidden + * @param index index of the download to restore + */ + void restoreDownload(int index); + + /** + * @brief cancel the specified download. This will lead to the corresponding file to be deleted + * + * @param index index of the download to cancel + **/ + void cancelDownload(int index); + + void pauseDownload(int index); + + void resumeDownload(int index); + + void queryInfo(int index); + + void visitOnNexus(int index); + + void openFile(int index); + + void openInDownloadsFolder(int index); + + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + + void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + + void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + + void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); + + void managedGameChanged(MOBase::IPluginGame const *gamePlugin); + +private slots: + + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadReadyRead(); + void downloadFinished(int index = 0); + void downloadError(QNetworkReply::NetworkError error); + void metaDataChanged(); + void directoryChanged(const QString &dirctory); + void checkDownloadTimeout(); + +private: + + void createMetaFile(DownloadInfo *info); + DownloadManager::DownloadInfo* getDownloadInfo(QString fileName); + +public: + + /** Get a unique filename for a download. + * + * This allows you multiple versions of download files, useful if the file + * comes from a web site with no version control + * + * @param basename: Name of the file + * + * @return Unique(ish) name + */ + QString getDownloadFileName(const QString &baseName, bool rename = false) const; + +private: + + void startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume); + void resumeDownloadInt(int index); + + /** + * @brief start a download from a url + * + * @param url the url to download from + * @param fileInfo information previously retrieved from the mod page + * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again + **/ + bool addDownload(const QStringList &URLs, QString gameName, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo); + + // important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time + DownloadInfo *findDownload(QObject *reply, int *index = nullptr) const; + + void removeFile(int index, bool deleteFile); + + void refreshAlphabeticalTranslation(); + + bool ByName(int LHS, int RHS); + + QString getFileNameFromNetworkReply(QNetworkReply *reply); + + void setState(DownloadInfo *info, DownloadManager::DownloadState state); + + DownloadInfo *downloadInfoByID(unsigned int id); + + void removePending(QString gameName, int modID, int fileID); + + static QString getFileTypeString(int fileType); + + void writeData(DownloadInfo *info); + +private: + + static const int AUTOMATIC_RETRIES = 3; + +private: + + NexusInterface *m_NexusInterface; + + QVector> m_PendingDownloads; + + QVector m_ActiveDownloads; + + QString m_OutputDirectory; + std::map m_PreferredServers; + QStringList m_SupportedExtensions; + std::set m_RequestIDs; + QVector m_AlphabeticalTranslation; + + QFileSystemWatcher m_DirWatcher; + + //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files + //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder. + //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui. + static int m_DirWatcherDisabler; + + + std::map m_DownloadFails; + + bool m_ShowHidden; + + MOBase::IPluginGame const *m_ManagedGame; + + QTimer m_TimeoutTimer; +}; + + + +#endif // DOWNLOADMANAGER_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9003d6b2..9dbb8a86 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5433,52 +5433,78 @@ void MainWindow::modDetailsUpdated(bool) } } -void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int) -{ - m_ModsToUpdate -= static_cast(modIDs.size()); - QVariantList resultList = resultData.toList(); - for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) { - QVariantMap result = iter->toMap(); - // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now - IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); - if (game - && result["id"].toInt() == game->nexusModOrganizerID() - && result["game_id"].toInt() == game->nexusGameID()) { - if (!result["voted_by_user"].toBool() && - Settings::instance().endorsementIntegration() && - !Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { - ui->actionEndorseMO->setVisible(true); - } - } else { - QString gameName = m_OrganizerCore.managedGame()->gameShortName(); - bool sameNexus = false; - for (IPluginGame *game : m_PluginContainer.plugins()) { - if (game->nexusGameID() == result["game_id"].toInt()) { - gameName = game->gameShortName(); - if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID()) - sameNexus = true; - break; +void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + QVariantMap resultInfo = resultData.toMap(); + QList files = resultInfo["files"].toList(); + QList fileUpdates = resultInfo["file_updates"].toList(); + bool foundUpdate = false; + m_ModsToUpdate--; + bool sameNexus = false; + for (IPluginGame *game : m_PluginContainer.plugins()) { + if (game->gameShortName() == gameName) { + if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID()) + sameNexus = true; + break; + } + } + std::vector modsList = ModInfo::getByModID(gameName, modID); + // Not clear to me what this is accomplishing? + //if (sameNexus) { + // std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), modID); + // info.reserve(info.size() + mainInfo.size()); + // info.insert(info.end(), mainInfo.begin(), mainInfo.end()); + //} + for (auto mod : modsList) { + QString installedFile = mod->getInstallationFile(); + for (auto update : fileUpdates) { + QVariantMap updateData = update.toMap(); + if (installedFile == updateData["old_file_name"].toString()) { + int currentUpdate = updateData["new_file_id"].toInt(); + bool finalUpdate = false; + while (!finalUpdate) { + finalUpdate = true; + for (auto updateScan : fileUpdates) { + QVariantMap updateScanData = updateScan.toMap(); + if (currentUpdate == updateScanData["old_file_id"].toInt()) { + currentUpdate = updateScanData["new_file_id"].toInt(); + finalUpdate = false; + break; + } + } } - } - std::vector info = ModInfo::getByModID(gameName, result["id"].toInt()); - if (sameNexus) { - std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), result["id"].toInt()); - info.reserve(info.size() + mainInfo.size()); - info.insert(info.end(), mainInfo.begin(), mainInfo.end()); - } - for (auto iter = info.begin(); iter != info.end(); ++iter) { - (*iter)->setNewestVersion(result["version"].toString()); - (*iter)->setNexusDescription(result["description"].toString()); - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated() && - result.contains("voted_by_user") && - Settings::instance().endorsementIntegration()) { - // don't use endorsement info if we're not logged in or if the response doesn't contain it - (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); + for (auto file : files) { + QVariantMap fileData = file.toMap(); + if (fileData["file_id"].toInt() == currentUpdate) { + mod->setNewestVersion(fileData["version"].toString()); + foundUpdate = true; + } } + + break; } } + + if (foundUpdate) { + mod->updateNXMInfo(); + } + else { + NexusInterface::instance(&m_PluginContainer)->requestDescription(gameName, modID, this, QVariant(), QString()); + } } + // Old endorsement and mod info updater + //for + // if (updateData["old_file_id"].toInt() == mod->readMeta()) + // (*iter)->setNewestVersion(result["version"].toString()); + //(*iter)->setNexusDescription(result["description"].toString()); + //if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated() && + // result.contains("voted_by_user") && + // Settings::instance().endorsementIntegration()) { + // // don't use endorsement info if we're not logged in or if the response doesn't contain it + // (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); + //} + if (m_ModsToUpdate <= 0) { statusBar()->hide(); m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); @@ -5488,18 +5514,41 @@ void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant us break; } } - } else { + } + else { m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); } } +void MainWindow::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + QVariantMap result = resultData.toMap(); + std::vector modsList = ModInfo::getByModID(gameName, modID); + for (auto mod : modsList) { + mod->setNexusDescription(result["description"].toString()); + + if ((mod->endorsedState() != ModInfo::ENDORSED_NEVER) && (result.contains("endorsement"))) { + QVariantMap endorsement = result["endorsement"].toMap(); + QString endorsementStatus = endorsement["endorse_status"].toString(); + if (endorsementStatus.compare("Endorsed") == 00) + mod->setIsEndorsed(true); + else if (endorsementStatus.compare("Abstained") == 00) + mod->setNeverEndorse(); + else + mod->setIsEndorsed(false); + } + } + disconnect(sender(), SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), + this, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int))); +} + void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int) { QMap results = resultData.toMap(); - if (results["code"].toInt() == 200) { + if (results["code"].toInt() == 200 || results["code"].toInt() == 201) { if (results["status"].toString().compare("Endorsed") == 0) { QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!")); - } else { + } else if (results["status"].toString().compare("Abstained") == 0) { QMessageBox::information(this, tr("Okay."), tr("This mod will not be endorsed and will no longer ask you to endorse.")); } ui->actionEndorseMO->setVisible(false); @@ -5529,14 +5578,22 @@ void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultDat } -void MainWindow::nxmRequestFailed(QString, int modID, int, QVariant, int, const QString &errorString) +void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, QNetworkReply::NetworkError error, const QString &errorString) { if (modID == -1) { // must be the update-check that failed m_ModsToUpdate = 0; statusBar()->hide(); } - MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); + if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) { + std::vector modsList = ModInfo::getByModID(gameName, modID); + for (auto mod : modsList) { + mod->setNexusID(-1); + } + MessageDialog::showMessage(tr("Mod ID %1 no longer seems to be available on Nexus.").arg(modID), this); + } else { + MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); + } } diff --git a/src/mainwindow.h b/src/mainwindow.h index caf94c0e..077022db 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -508,10 +508,11 @@ private slots: void modInstalled(const QString &modName); - void nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int requestID); + void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, int requestID, const QString &errorString); + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); void editCategories(); void deselectFilters(); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 905341b0..e7b7657b 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -285,30 +285,15 @@ ModInfo::ModInfo(PluginContainer *pluginContainer) } -void ModInfo::checkChunkForUpdate(PluginContainer *pluginContainer, const std::vector &modIDs, QObject *receiver, QString gameName) -{ - if (modIDs.size() != 0) { - NexusInterface::instance(pluginContainer)->requestUpdates(modIDs, receiver, QVariant(), gameName, QString()); - } -} - - int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) { - // technically this should be 255 but those requests can take nexus fairly long, produce - // large output and may have been the cause of issue #1166 - static const int chunkSize = 64; - int result = 0; - std::vector modIDs; - - // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now - IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition"); - if (game && game->nexusModOrganizerID()) { - modIDs.push_back(game->nexusModOrganizerID()); - checkChunkForUpdate(pluginContainer, modIDs, receiver, game->gameShortName()); - modIDs.clear(); - } + + // MO2 endorsement status is no longer available via this method - an alternative must be found + //IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition"); + //if (game && game->nexusModOrganizerID()) { + // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); + //} std::multimap> organizedGames; for (auto mod : s_Collection) { @@ -317,24 +302,10 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } } - QString currentGame = ""; for (auto game : organizedGames) { - if (currentGame != game.first) { - if (currentGame != "") { - checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame); - modIDs.clear(); - } - currentGame = game.first; - } - modIDs.push_back(game.second->getNexusID()); - if (modIDs.size() >= chunkSize) { - checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame); - modIDs.clear(); - } + NexusInterface::instance(pluginContainer)->requestUpdates(game.second->getNexusID(), receiver, QVariant(), game.first, QString()); } - checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame); - return result; } diff --git a/src/modinfobackup.h b/src/modinfobackup.h index da1fcd4a..f74cd111 100644 --- a/src/modinfobackup.h +++ b/src/modinfobackup.h @@ -17,6 +17,7 @@ public: virtual void setGameName(QString) {} virtual void setNexusID(int) {} virtual void endorse(bool) {} + virtual void parseNexusInfo() {} virtual int getFixedPriority() const { return -1; } virtual void ignoreUpdate(bool) {} virtual bool canBeUpdated() const { return false; } diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index c2ded812..d086de08 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -1,1577 +1,1577 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "modinfodialog.h" -#include "ui_modinfodialog.h" -#include "descriptionpage.h" -#include "mainwindow.h" - -#include "modidlineedit.h" -#include "iplugingame.h" -#include "nexusinterface.h" -#include "report.h" -#include "utility.h" -#include "messagedialog.h" -#include "bbcode.h" -#include "questionboxmemory.h" -#include "settings.h" -#include "categories.h" -#include "organizercore.h" -#include "pluginlistsortproxy.h" -#include "previewgenerator.h" -#include "previewdialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - - -using namespace MOBase; -using namespace MOShared; - - -class ModFileListWidget : public QListWidgetItem { - friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS); -public: - ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0) - : QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {} -private: - int m_SortValue; -}; - - -static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS) -{ - return LHS.m_SortValue < RHS.m_SortValue; -} - - -ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent) - : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo), - m_ThumbnailMapper(this), m_RequestStarted(false), - m_DeleteAction(nullptr), m_RenameAction(nullptr), m_OpenAction(nullptr), - m_Directory(directory), m_Origin(nullptr), - m_OrganizerCore(organizerCore), m_PluginContainer(pluginContainer) -{ - ui->setupUi(this); - this->setWindowTitle(modInfo->name()); - this->setWindowModality(Qt::WindowModal); - - m_RootPath = modInfo->absolutePath(); - - QString metaFileName = m_RootPath.mid(0).append("/meta.ini"); - m_Settings = new QSettings(metaFileName, QSettings::IniFormat); - - QLineEdit *modIDEdit = findChild("modIDEdit"); - ui->modIDEdit->setValidator(new QIntValidator(modIDEdit)); - ui->modIDEdit->setText(QString("%1").arg(modInfo->getNexusID())); - - connect(ui->modIDEdit, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); - - QString gameName = modInfo->getGameName(); - ui->sourceGameEdit->addItem(organizerCore->managedGame()->gameName(), organizerCore->managedGame()->gameShortName()); - if (organizerCore->managedGame()->validShortNames().size() == 0) { - ui->sourceGameEdit->setDisabled(true); - } else { - for (auto game : pluginContainer->plugins()) { - for (QString gameName : organizerCore->managedGame()->validShortNames()) { - if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { - ui->sourceGameEdit->addItem(game->gameName(), game->gameShortName()); - break; - } - } - } - } - ui->sourceGameEdit->setCurrentIndex(ui->sourceGameEdit->findData(gameName)); - - ui->commentsEdit->setText(modInfo->comments()); - ui->notesEdit->setText(modInfo->notes()); - - ui->descriptionView->setPage(new DescriptionPage()); - - connect(&m_ThumbnailMapper, SIGNAL(mapped(const QString&)), this, SIGNAL(thumbnailClickedSignal(const QString&))); - connect(this, SIGNAL(thumbnailClickedSignal(const QString&)), this, SLOT(thumbnailClicked(const QString&))); - connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool))); - connect(ui->descriptionView->page(), SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl))); - //TODO: No easy way to delegate links - //ui->descriptionView->page()->acceptNavigationRequest(QWebEnginePage::DelegateAllLinks); - - new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated())); - - if (directory->originExists(ToWString(modInfo->name()))) { - m_Origin = &directory->getOriginByName(ToWString(modInfo->name())); - if (m_Origin->isDisabled()) { - m_Origin = nullptr; - } - } - - refreshLists(); - - if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) - { - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); - ui->tabWidget->setTabEnabled(TAB_INIFILES, false); - ui->tabWidget->setTabEnabled(TAB_IMAGES, false); - ui->tabWidget->setTabEnabled(TAB_ESPS, false); - ui->tabWidget->setTabEnabled(TAB_CONFLICTS, false); - //ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); - addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); - refreshPrimaryCategoriesBox(); - ui->tabWidget->setTabEnabled(TAB_NEXUS, false); - //ui->tabWidget->setTabEnabled(TAB_NOTES, false); - ui->tabWidget->setTabEnabled(TAB_FILETREE, false); - } - else if (unmanaged) - { - ui->tabWidget->setTabEnabled(TAB_INIFILES, false); - ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); - ui->tabWidget->setTabEnabled(TAB_NEXUS, false); - ui->tabWidget->setTabEnabled(TAB_FILETREE, false); - ui->tabWidget->setTabEnabled(TAB_NOTES, false); - ui->tabWidget->setTabEnabled(TAB_ESPS, false); - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); - ui->tabWidget->setTabEnabled(TAB_IMAGES, false); - } else { - initFiletree(modInfo); - addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); - refreshPrimaryCategoriesBox(); - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, ui->textFileList->count() != 0); - ui->tabWidget->setTabEnabled(TAB_IMAGES, ui->thumbnailArea->count() != 0); - ui->tabWidget->setTabEnabled(TAB_ESPS, (ui->inactiveESPList->count() != 0) || (ui->activeESPList->count() != 0)); - } - initINITweaks(); - - ui->tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != nullptr); - - - ui->endorseBtn->setVisible(Settings::instance().endorsementIntegration()); - ui->endorseBtn->setEnabled((m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE) || - (m_ModInfo->endorsedState() == ModInfo::ENDORSED_NEVER)); - - // activate first enabled tab - for (int i = 0; i < ui->tabWidget->count(); ++i) { - if (ui->tabWidget->isTabEnabled(i)) { - ui->tabWidget->setCurrentIndex(i); - break; - } - } - - if (ui->tabWidget->currentIndex() == TAB_NEXUS) { - activateNexusTab(); - } -} - - -ModInfoDialog::~ModInfoDialog() -{ - m_ModInfo->setComments(ui->commentsEdit->text()); - //Avoid saving html stump if notes field is empty. - if (ui->notesEdit->toPlainText().isEmpty()) - m_ModInfo->setNotes(ui->notesEdit->toPlainText()); - else - m_ModInfo->setNotes(ui->notesEdit->toHtml()); - saveCategories(ui->categoriesTree->invisibleRootItem()); - saveIniTweaks(); // ini tweaks are written to the ini file directly. This is the only information not managed by ModInfo - delete ui->descriptionView->page(); - delete ui->descriptionView; - delete ui; - delete m_Settings; -} - - -void ModInfoDialog::initINITweaks() -{ - int numTweaks = m_Settings->beginReadArray("INI Tweaks"); - for (int i = 0; i < numTweaks; ++i) { - m_Settings->setArrayIndex(i); - QList items = ui->iniTweaksList->findItems(m_Settings->value("name").toString(), Qt::MatchFixedString); - if (items.size() != 0) { - items.at(0)->setCheckState(Qt::Checked); - } - } - m_Settings->endArray(); -} - -void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo) -{ - ui->fileTree = findChild("fileTree"); - - m_FileSystemModel = new QFileSystemModel(this); - m_FileSystemModel->setReadOnly(false); - m_FileSystemModel->setRootPath(m_RootPath); - ui->fileTree->setModel(m_FileSystemModel); - ui->fileTree->setRootIndex(m_FileSystemModel->index(m_RootPath)); - ui->fileTree->setColumnWidth(0, 300); - - m_DeleteAction = new QAction(tr("&Delete"), ui->fileTree); - m_RenameAction = new QAction(tr("&Rename"), ui->fileTree); - m_HideAction = new QAction(tr("&Hide"), ui->fileTree); - m_UnhideAction = new QAction(tr("&Unhide"), ui->fileTree); - m_OpenAction = new QAction(tr("&Open"), ui->fileTree); - m_NewFolderAction = new QAction(tr("&New Folder"), ui->fileTree); - QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered())); - QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered())); - QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered())); - QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered())); - QObject::connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered())); - connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered())); -} - - -int ModInfoDialog::tabIndex(const QString &tabId) -{ - for (int i = 0; i < ui->tabWidget->count(); ++i) { - if (ui->tabWidget->widget(i)->objectName() == tabId) { - return i; - } - } - return -1; -} - - -void ModInfoDialog::restoreTabState(const QByteArray &state) -{ - QDataStream stream(state); - int count = 0; - stream >> count; - - QStringList tabIds; - - // first, only determine the new mapping - for (int newPos = 0; newPos < count; ++newPos) { - QString tabId; - stream >> tabId; - tabIds.append(tabId); - int oldPos = tabIndex(tabId); - if (oldPos != -1) { - m_RealTabPos[newPos] = oldPos; - } else { - m_RealTabPos[newPos] = newPos; - } - } - // then actually move the tabs - QTabBar *tabBar = ui->tabWidget->findChild("qt_tabwidget_tabbar"); // magic name = bad - ui->tabWidget->blockSignals(true); - for (int newPos = 0; newPos < count; ++newPos) { - QString tabId = tabIds.at(newPos); - int oldPos = tabIndex(tabId); - tabBar->moveTab(oldPos, newPos); - } - ui->tabWidget->blockSignals(false); -} - - -QByteArray ModInfoDialog::saveTabState() const -{ - QByteArray result; - QDataStream stream(&result, QIODevice::WriteOnly); - stream << ui->tabWidget->count(); - for (int i = 0; i < ui->tabWidget->count(); ++i) { - stream << ui->tabWidget->widget(i)->objectName(); - } - - return result; -} - - -void ModInfoDialog::refreshLists() -{ - int numNonConflicting = 0; - int numOverwrite = 0; - int numOverwritten = 0; - - ui->overwriteTree->clear(); - ui->overwrittenTree->clear(); - - if (m_Origin != nullptr) { - std::vector files = m_Origin->getFiles(); - for (auto iter = files.begin(); iter != files.end(); ++iter) { - QString relativeName = QDir::fromNativeSeparators(ToQString((*iter)->getRelativePath())); - QString fileName = relativeName.mid(0).prepend(m_RootPath); - bool archive; - if ((*iter)->getOrigin(archive) == m_Origin->getID()) { - std::vector>> alternatives = (*iter)->getAlternatives(); - if (!alternatives.empty()) { - std::wostringstream altString; - for (std::vector>>::iterator altIter = alternatives.begin(); - altIter != alternatives.end(); ++altIter) { - if (altIter != alternatives.begin()) { - altString << ", "; - } - altString << m_Directory->getOriginByID(altIter->first).getName(); - } - QStringList fields(relativeName.prepend("...")); - fields.append(ToQString(altString.str())); - - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(0, Qt::UserRole, fileName); - item->setData(1, Qt::UserRole, ToQString(m_Directory->getOriginByID(alternatives.back().first).getName())); - item->setData(1, Qt::UserRole + 1, alternatives.back().first); - item->setData(1, Qt::UserRole + 2, archive); - if (archive) { - QFont font = item->font(0); - font.setItalic(true); - item->setFont(0, font); - item->setFont(1, font); - } - ui->overwriteTree->addTopLevelItem(item); - ++numOverwrite; - } else {// otherwise don't display the file - ++numNonConflicting; - } - } else { - FilesOrigin &realOrigin = m_Directory->getOriginByID((*iter)->getOrigin(archive)); - QStringList fields(relativeName); - fields.append(ToQString(realOrigin.getName())); - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(0, Qt::UserRole, fileName); - item->setData(1, Qt::UserRole, ToQString(realOrigin.getName())); - item->setData(1, Qt::UserRole + 2, archive); - if (archive) { - QFont font = item->font(0); - font.setItalic(true); - item->setFont(0, font); - item->setFont(1, font); - } - ui->overwrittenTree->addTopLevelItem(item); - ++numOverwritten; - } - } - } - - if (m_RootPath.length() > 0) { - QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories); - while (dirIterator.hasNext()) { - QString fileName = dirIterator.next(); - - if (fileName.endsWith(".txt", Qt::CaseInsensitive)) { - ui->textFileList->addItem(fileName.mid(m_RootPath.length() + 1)); - } else if ((fileName.endsWith(".ini", Qt::CaseInsensitive) || fileName.endsWith(".cfg", Qt::CaseInsensitive)) && - !fileName.endsWith("meta.ini")) { - QString namePart = fileName.mid(m_RootPath.length() + 1); - if (namePart.startsWith("INI Tweaks", Qt::CaseInsensitive)) { - QListWidgetItem *newItem = new QListWidgetItem(namePart.mid(11), ui->iniTweaksList); - newItem->setData(Qt::UserRole, namePart); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(Qt::Unchecked); - ui->iniTweaksList->addItem(newItem); - } else { - ui->iniFileList->addItem(namePart); - } - } else if (fileName.endsWith(".esp", Qt::CaseInsensitive) || - fileName.endsWith(".esm", Qt::CaseInsensitive) || - fileName.endsWith(".esl", Qt::CaseInsensitive)) { - QString relativePath = fileName.mid(m_RootPath.length() + 1); - if (relativePath.contains('/')) { - QFileInfo fileInfo(fileName); - QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName()); - newItem->setData(Qt::UserRole, relativePath); - ui->inactiveESPList->addItem(newItem); - } else { - ui->activeESPList->addItem(relativePath); - } - } else if ((fileName.endsWith(".png", Qt::CaseInsensitive)) || - (fileName.endsWith(".jpg", Qt::CaseInsensitive))) { - QImage image = QImage(fileName); - if (!image.isNull()) { - if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { - image = image.scaledToWidth(128); - } else { - image = image.scaledToHeight(96); - } - - QPushButton *thumbnailButton = new QPushButton(QPixmap::fromImage(image), ""); - thumbnailButton->setIconSize(QSize(image.width(), image.height())); - connect(thumbnailButton, SIGNAL(clicked()), &m_ThumbnailMapper, SLOT(map())); - m_ThumbnailMapper.setMapping(thumbnailButton, fileName); - ui->thumbnailArea->addWidget(thumbnailButton); - } - } - } - } - - ui->overwriteCount->display(numOverwrite); - ui->overwrittenCount->display(numOverwritten); - ui->noConflictCount->display(numNonConflicting); -} - - -void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel) -{ - for (int i = 0; i < static_cast(factory.numCategories()); ++i) { - if (factory.getParentID(i) != rootLevel) { - continue; - } - int categoryID = factory.getCategoryID(i); - QTreeWidgetItem *newItem - = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(0, enabledCategories.find(categoryID) - != enabledCategories.end() - ? Qt::Checked - : Qt::Unchecked); - newItem->setData(0, Qt::UserRole, categoryID); - if (factory.hasChildren(i)) { - addCategories(factory, enabledCategories, newItem, categoryID); - } - root->addChild(newItem); - } -} - - -void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode) -{ - for (int i = 0; i < currentNode->childCount(); ++i) { - QTreeWidgetItem *childNode = currentNode->child(i); - m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); - saveCategories(childNode); - } -} - - -void ModInfoDialog::on_closeButton_clicked() -{ - if (allowNavigateFromTXT() && allowNavigateFromINI()) { - this->close(); - } -} - - - -QString ModInfoDialog::getModVersion() const -{ - return m_Settings->value("version", "").toString(); -} - - -const int ModInfoDialog::getModID() const -{ - return m_Settings->value("modid", 0).toInt(); -} - -void ModInfoDialog::openTab(int tab) -{ - QTabWidget *tabWidget = findChild("tabWidget"); - if (tabWidget->isTabEnabled(tab)) { - tabWidget->setCurrentIndex(tab); - } -} - -void ModInfoDialog::thumbnailClicked(const QString &fileName) -{ - QLabel *imageLabel = findChild("imageLabel"); - imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); - QImage image(fileName); - if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { - image = image.scaledToWidth(imageLabel->geometry().width()); - } else { - image = image.scaledToHeight(imageLabel->geometry().height()); - } - imageLabel->setPixmap(QPixmap::fromImage(image)); -} - -bool ModInfoDialog::allowNavigateFromTXT() -{ - if (ui->saveTXTButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->textFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentTextFile(); - } - } - return true; -} - - -bool ModInfoDialog::allowNavigateFromINI() -{ - if (ui->saveButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->iniFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentIniFile(); - } - } - return true; -} - - -void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->text(); - - QVariant currentFile = ui->textFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromTXT()) { - openTextFile(fullPath); - } else { - ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } -} - - -void ModInfoDialog::openTextFile(const QString &fileName) -{ - QString encoding; - ui->textFileView->setText(MOBase::readFileText(fileName, &encoding)); - ui->textFileView->setProperty("currentFile", fileName); - ui->textFileView->setProperty("encoding", encoding); - ui->saveTXTButton->setEnabled(false); -} - - -void ModInfoDialog::openIniFile(const QString &fileName) -{ - QFile iniFile(fileName); - iniFile.open(QIODevice::ReadOnly); - QByteArray buffer = iniFile.readAll(); - - QTextCodec *codec = QTextCodec::codecForUtfText(buffer, QTextCodec::codecForName("utf-8")); - QTextEdit *iniFileView = findChild("iniFileView"); - iniFileView->setText(codec->toUnicode(buffer)); - iniFileView->setProperty("currentFile", fileName); - iniFileView->setProperty("encoding", codec->name()); - iniFile.close(); - - ui->saveButton->setEnabled(false); -} - - -void ModInfoDialog::saveIniTweaks() -{ - m_Settings->remove("INI Tweaks"); - m_Settings->beginWriteArray("INI Tweaks"); - - int countEnabled = 0; - for (int i = 0; i < ui->iniTweaksList->count(); ++i) { - if (ui->iniTweaksList->item(i)->checkState() == Qt::Checked) { - m_Settings->setArrayIndex(countEnabled++); - m_Settings->setValue("name", ui->iniTweaksList->item(i)->text()); - } - } - m_Settings->endArray(); -} - - -void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->text(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } -} - - -void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->data(Qt::UserRole).toString(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } - -} - - -void ModInfoDialog::on_saveButton_clicked() -{ - saveCurrentIniFile(); -} - - -void ModInfoDialog::on_saveTXTButton_clicked() -{ - saveCurrentTextFile(); -} - - -void ModInfoDialog::saveCurrentTextFile() -{ - QVariant fileNameVar = ui->textFileView->property("currentFile"); - QVariant encodingVar = ui->textFileView->property("encoding"); - if (fileNameVar.isValid() && encodingVar.isValid()) { - QString fileName = fileNameVar.toString(); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->textFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); - } - ui->saveTXTButton->setEnabled(false); -} - - -void ModInfoDialog::saveCurrentIniFile() -{ - QVariant fileNameVar = ui->iniFileView->property("currentFile"); - QVariant encodingVar = ui->iniFileView->property("encoding"); - if (fileNameVar.isValid() && !fileNameVar.toString().isEmpty()) { - QString fileName = fileNameVar.toString(); - QDir().mkpath(QFileInfo(fileName).absolutePath()); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->iniFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); - } - ui->saveButton->setEnabled(false); -} - - -void ModInfoDialog::on_iniFileView_textChanged() -{ - QPushButton* saveButton = findChild("saveButton"); - saveButton->setEnabled(true); -} - - -void ModInfoDialog::on_textFileView_textChanged() -{ - ui->saveTXTButton->setEnabled(true); -} - - -void ModInfoDialog::on_activateESP_clicked() -{ - QListWidget *activeESPList = findChild("activeESPList"); - QListWidget *inactiveESPList = findChild("inactiveESPList"); - - int selectedRow = inactiveESPList->currentRow(); - if (selectedRow < 0) { - return; - } - - QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow); - - QDir root(m_RootPath); - bool renamed = false; - - while (root.exists(selectedItem->text())) { - bool okClicked = false; - QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked); - if (!okClicked) { - inactiveESPList->insertItem(selectedRow, selectedItem); - return; - } else if (newName.size() > 0) { - selectedItem->setText(newName); - renamed = true; - } - } - - if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) { - activeESPList->addItem(selectedItem); - if (renamed) { - selectedItem->setData(Qt::UserRole, QVariant()); - } - } else { - inactiveESPList->insertItem(selectedRow, selectedItem); - reportError(tr("failed to move file")); - } -} - - -void ModInfoDialog::on_deactivateESP_clicked() -{ - QListWidget *activeESPList = findChild("activeESPList"); - QListWidget *inactiveESPList = findChild("inactiveESPList"); - - int selectedRow = activeESPList->currentRow(); - if (selectedRow < 0) { - return; - } - - QDir root(m_RootPath); - - QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow); - - // if we moved the file from optional to active in this session, we move the file back to - // where it came from. Otherwise, it is moved to the new folder "optional" - if (selectedItem->data(Qt::UserRole).isNull()) { - selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text()); - if (!root.exists("optional")) { - if (!root.mkdir("optional")) { - reportError(tr("failed to create directory \"optional\"")); - activeESPList->insertItem(selectedRow, selectedItem); - return; - } - } - } - - if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) { - inactiveESPList->addItem(selectedItem); - } else { - activeESPList->insertItem(selectedRow, selectedItem); - } -} - -void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link) -{ - emit linkActivated(link); -} - -void ModInfoDialog::linkClicked(const QUrl &url) -{ - //Ideally we'd ask the mod for the game and the web service then pass the game - //and URL to the web service - if (NexusInterface::instance(m_PluginContainer)->isURLGameRelated(url)) { - - emit linkActivated(url.toString()); - } else { - ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } -} - -void ModInfoDialog::linkClicked(QString url) -{ - emit linkActivated(url); -} - - -void ModInfoDialog::refreshNexusData(int modID) -{ - if ((!m_RequestStarted) && (modID > 0)) { - m_RequestStarted = true; - - m_ModInfo->updateNXMInfo(); - - MessageDialog::showMessage(tr("Info requested, please wait"), this); - } -} - - -/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - QVariantMap result = resultData.toMap(); - - if (!result["description"].isNull()) { - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(result["description"].toString())); - -// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - ui->descriptionView->setHtml(descriptionAsHTML); - } else { - ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - } - - QLineEdit *versionEdit = findChild("versionEdit"); - QString version = result["version"].toString(); - - if (!version.isEmpty()) { - m_ModInfo->setNewestVersion(version); - - VersionInfo currentVersion(versionEdit->text()); - VersionInfo newestVersion(version); - - QPalette versionColor; - if (currentVersion < newestVersion) { - versionColor.setColor(QPalette::Text, Qt::red); - versionEdit->setToolTip(tr("Current Version: %1").arg(version)); - } else { - versionColor.setColor(QPalette::Text, Qt::green); - versionEdit->setToolTip(tr("No update available")); - } - versionEdit->setPalette(versionColor); - } -}*/ - - -QString ModInfoDialog::getFileCategory(int categoryID) -{ - switch (categoryID) { - case 1: return tr("Main"); - case 2: return tr("Update"); - case 3: return tr("Optional"); - case 4: return tr("Old"); - case 5: return tr("Misc"); - default: return tr("Unknown"); - } -} - - -void ModInfoDialog::updateVersionColor() -{ -// QPalette versionColor; - if (m_ModInfo->getVersion() != m_ModInfo->getNewestVersion()) { - ui->versionEdit->setStyleSheet("color: red"); -// versionColor.setColor(QPalette::Text, Qt::red); - ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString())); - } else { - ui->versionEdit->setStyleSheet("color: green"); -// versionColor.setColor(QPalette::Text, Qt::green); - ui->versionEdit->setToolTip(tr("No update available")); - } -// ui->versionEdit->setPalette(versionColor); -} - - -void ModInfoDialog::modDetailsUpdated(bool success) -{ - if (success) { - QString nexusDescription = m_ModInfo->getNexusDescription(); - if (!nexusDescription.isEmpty()) { - /* QString input = - "[size=20]sizetest[/size]\r\n" - "[COLOR=yellow]colortest[/COLOR]\r\n" - "[center]centertest[/center]\r\n" - "[quote]quotetest 1[/quote]\r\n" - "[quote=bla]quotetest 2[/quote]\r\n" - "[url]www.skyrimnexus.com[/url]\r\n" - "[url=www.skyrimnexus.com]urltest 2[/url]\r\n" - "[ol]\r\n" - "[li]item 2[/li]" - "[*]item 1\r\n" - "[/ol]\r\n" - "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n" - "[table][tr][th]headertest1[/th]" - "[th]headertest2[/th][/tr]" - "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]" - "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]" - "[email=\"sherb@gmx.net\"]mail me[/email]"; - ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/ - - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(nexusDescription)); - - ui->descriptionView->page()->setHtml(descriptionAsHTML); - - // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - // ui->descriptionView->setHtml(descriptionAsHTML); - } else { - // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - ui->descriptionView->page()->setHtml(tr("(description incomplete, please visit nexus)")); - } - - updateVersionColor(); - } -} - - -void ModInfoDialog::activateNexusTab() -{ - QLineEdit *modIDEdit = findChild("modIDEdit"); - int modID = modIDEdit->text().toInt(); - if (modID != 0) { - QString nexusLink = NexusInterface::instance(m_PluginContainer)->getModURL(modID, m_ModInfo->getGameName()); - QLabel *visitNexusLabel = findChild("visitNexusLabel"); - visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink)); - visitNexusLabel->setToolTip(nexusLink); - - if (m_ModInfo->getNexusDescription().isEmpty() || - QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { - refreshNexusData(modID); - } else { - this->modDetailsUpdated(true); - } - } - QLineEdit *versionEdit = findChild("versionEdit"); - QString currentVersion = m_Settings->value("version", "0.0").toString(); - versionEdit->setText(currentVersion); - ui->customUrlLineEdit->setText(m_ModInfo->getURL()); -} - - -void ModInfoDialog::on_tabWidget_currentChanged(int index) -{ - if (index == TAB_NEXUS || m_RealTabPos[index] == TAB_NEXUS) { - activateNexusTab(); - } -} - - -void ModInfoDialog::on_modIDEdit_editingFinished() -{ - int oldID = m_Settings->value("modid", 0).toInt(); - int modID = ui->modIDEdit->text().toInt(); - if (oldID != modID){ - m_ModInfo->setNexusID(modID); - - ui->descriptionView->page()->setHtml(""); - if (modID != 0) { - m_RequestStarted = false; - refreshNexusData(modID); - } - } -} - -void ModInfoDialog::on_sourceGameEdit_currentIndexChanged(int) -{ - for (auto game : m_PluginContainer->plugins()) { - if (game->gameName() == ui->sourceGameEdit->currentText()) { - m_ModInfo->setGameName(game->gameShortName()); - return; - } - } -} - -void ModInfoDialog::on_versionEdit_editingFinished() -{ - VersionInfo version(ui->versionEdit->text()); - m_ModInfo->setVersion(version); - updateVersionColor(); -} - -void ModInfoDialog::on_customUrlLineEdit_editingFinished() -{ - m_ModInfo->setURL(ui->customUrlLineEdit->text()); -} - -bool ModInfoDialog::recursiveDelete(const QModelIndex &index) -{ - for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) { - QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index); - if (m_FileSystemModel->isDir(childIndex)) { - if (!recursiveDelete(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } else { - if (!m_FileSystemModel->remove(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } - } - if (!m_FileSystemModel->remove(index)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData()); - return false; - } - return true; -} - - -void ModInfoDialog::on_openInExplorerButton_clicked() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void ModInfoDialog::deleteFile(const QModelIndex &index) -{ - - bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index) - : m_FileSystemModel->remove(index); - if (!res) { - QString fileName = m_FileSystemModel->fileName(index); - reportError(tr("Failed to delete %1").arg(fileName)); - } -} - -void ModInfoDialog::delete_activated() -{ - if (ui->fileTree->hasFocus()) { - QItemSelectionModel *selection = ui->fileTree->selectionModel(); - - if (selection->hasSelection() && selection->selectedRows().count() >= 1) { - - if (selection->selectedRows().count() == 0) { - return; - } - else if (selection->selectedRows().count() == 1) { - QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0)); - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - else { - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - - foreach(QModelIndex index, selection->selectedRows()) { - deleteFile(index); - } - } - } -} - -void ModInfoDialog::deleteTriggered() -{ - if (m_FileSelection.count() == 0) { - return; - } else if (m_FileSelection.count() == 1) { - QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0)); - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } else { - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - - foreach(QModelIndex index, m_FileSelection) { - deleteFile(index); - } -} - - -void ModInfoDialog::renameTriggered() -{ - QModelIndex selection = m_FileSelection.at(0); - QModelIndex index = selection.sibling(selection.row(), 0); - if (!index.isValid() || m_FileSystemModel->isReadOnly()) { - return; - } - - ui->fileTree->edit(index); -} - - -void ModInfoDialog::hideTriggered() -{ - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (!path.endsWith(ModInfo::s_HiddenExt)) { - hideFile(path); - } - } -} - - -void ModInfoDialog::unhideTriggered() -{ - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (path.endsWith(ModInfo::s_HiddenExt)) { - unhideFile(path); - } - } -} - - -void ModInfoDialog::openFile(const QModelIndex &index) -{ - QString fileName = m_FileSystemModel->filePath(index); - - HINSTANCE res = ::ShellExecuteW(nullptr, L"open", ToWString(fileName).c_str(), nullptr, nullptr, SW_SHOW); - if ((unsigned long long)res <= 32) { - qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res); - } -} - - -void ModInfoDialog::openTriggered() -{ - foreach(QModelIndex idx, m_FileSelection) { - openFile(idx); - } -} - -void ModInfoDialog::createDirectoryTriggered() -{ - QModelIndex selection = m_FileSelection.at(0); - - QModelIndex index = m_FileSystemModel->isDir(selection) ? selection - : selection.parent(); - index = index.sibling(index.row(), 0); - - QString name = tr("New Folder"); - QString path = m_FileSystemModel->filePath(index).append("/"); - - QModelIndex existingIndex = m_FileSystemModel->index(path + name); - int suffix = 1; - while (existingIndex.isValid()) { - name = tr("New Folder") + QString::number(suffix++); - existingIndex = m_FileSystemModel->index(path + name); - } - - QModelIndex newIndex = m_FileSystemModel->mkdir(index, name); - if (!newIndex.isValid()) { - reportError(tr("Failed to create \"%1\"").arg(name)); - return; - } - - ui->fileTree->setCurrentIndex(newIndex); - ui->fileTree->edit(newIndex); -} - - -void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos) -{ - QItemSelectionModel *selectionModel = ui->fileTree->selectionModel(); - m_FileSelection = selectionModel->selectedRows(0); - -// m_FileSelection = ui->fileTree->indexAt(pos); - QMenu menu(ui->fileTree); - - menu.addAction(m_NewFolderAction); - - bool hasFiles = false; - - foreach(QModelIndex idx, m_FileSelection) { - if (m_FileSystemModel->fileInfo(idx).isFile()) { - hasFiles = true; - break; - } - } - - if (selectionModel->hasSelection()) { - if (hasFiles) { - menu.addAction(m_OpenAction); - } - menu.addAction(m_RenameAction); - menu.addAction(m_DeleteAction); - if (m_FileSystemModel->fileName(m_FileSelection.at(0)).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(m_UnhideAction); - } else { - menu.addAction(m_HideAction); - } - } else { - m_FileSelection.clear(); - m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0)); - } - menu.exec(ui->fileTree->mapToGlobal(pos)); -} - - -void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int) -{ - QTreeWidgetItem *parent = item->parent(); - while ((parent != nullptr) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { - parent->setCheckState(0, Qt::Checked); - parent = parent->parent(); - } - refreshPrimaryCategoriesBox(); -} - - -void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree) -{ - for (int i = 0; i < tree->childCount(); ++i) { - QTreeWidgetItem *child = tree->child(i); - if (child->checkState(0) == Qt::Checked) { - ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole)); - addCheckedCategories(child); - } - } -} - - -void ModInfoDialog::refreshPrimaryCategoriesBox() -{ - ui->primaryCategoryBox->clear(); - int primaryCategory = m_ModInfo->getPrimaryCategory(); - addCheckedCategories(ui->categoriesTree->invisibleRootItem()); - for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) { - if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) { - ui->primaryCategoryBox->setCurrentIndex(i); - break; - } - } -} - - -void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index) -{ - if (index != -1) { - m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt()); - } -} - - -void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int) -{ - this->close(); - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); -} - - -bool ModInfoDialog::hideFile(const QString &oldName) -{ - QString newName = oldName + ModInfo::s_HiddenExt; - - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { - return false; - } - } - - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName))); - return false; - } -} - - -bool ModInfoDialog::unhideFile(const QString &oldName) -{ - QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { - return false; - } - } - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); - return false; - } -} - - -void ModInfoDialog::hideConflictFile() -{ - if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - emit originModified(m_Origin->getID()); - refreshLists(); - } -} - - -void ModInfoDialog::unhideConflictFile() -{ - if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - emit originModified(m_Origin->getID()); - refreshLists(); - } -} - -int ModInfoDialog::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) -{ - QString extension = targetInfo.suffix(); - if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || - (extension.compare("com", Qt::CaseInsensitive) == 0) || - (extension.compare("bat", Qt::CaseInsensitive) == 0)) { - binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); - arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - return 1; - } - else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { - binaryInfo = targetInfo; - return 1; - } - else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { - // types that need to be injected into - std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); - QString binaryPath; - - { // try to find java automatically - WCHAR buffer[MAX_PATH]; - if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(buffer, &binaryType)) { - qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); - } - else if (binaryType == SCS_32BIT_BINARY) { - binaryPath = ToQString(buffer); - } - } - } - if (binaryPath.isEmpty() && (extension == "jar")) { - // second attempt: look to the registry - QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (javaReg.contains("CurrentVersion")) { - QString currentVersion = javaReg.value("CurrentVersion").toString(); - binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); - } - } - if (binaryPath.isEmpty()) { - binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); - } - if (binaryPath.isEmpty()) { - return 0; - } - binaryInfo = QFileInfo(binaryPath); - if (extension == "jar") { - arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - else { - arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - return 1; - } - else { - return 2; - } -} - -void ModInfoDialog::openDataFile() -{ - if (m_ConflictsContextItem != nullptr) { - QFileInfo targetInfo(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - m_OrganizerCore->spawnBinaryDirect( - binaryInfo, arguments, m_OrganizerCore->currentProfile()->name(), - targetInfo.absolutePath(), "", ""); - } break; - case 2: { - ::ShellExecuteW(nullptr, L"open", - ToWString(targetInfo.absoluteFilePath()).c_str(), - nullptr, nullptr, SW_SHOWNORMAL); - } break; - default: { - // nop - } break; - } - } -} - -void ModInfoDialog::previewDataFile() -{ - QString fileName = QDir::fromNativeSeparators(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); - - // what we have is an absolute path to the file in its actual location (for the primary origin) - // what we want is the path relative to the virtual data directory - - // we need to look in the virtual directory for the file to make sure the info is up to date. - - // check if the file comes from the actual data folder instead of a mod - QDir gameDirectory = m_OrganizerCore->managedGame()->dataDirectory().absolutePath(); - QString relativePath = gameDirectory.relativeFilePath(fileName); - QDir direRelativePath = gameDirectory.relativeFilePath(fileName); - // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case - if (!direRelativePath.isAbsolute() && !relativePath.startsWith("..")) { - fileName = relativePath; - } - else { - // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory - int offset = m_OrganizerCore->settings().getModDirectory().size() + 1; - offset = fileName.indexOf("/", offset); - fileName = fileName.mid(offset + 1); - } - - - - const FileEntry::Ptr file = m_OrganizerCore->directoryStructure()->searchFile(ToWString(fileName), nullptr); - - if (file.get() == nullptr) { - reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); - return; - } - - // set up preview dialog - PreviewDialog preview(fileName); - auto addFunc = [&](int originId) { - FilesOrigin &origin = m_OrganizerCore->directoryStructure()->getOriginByID(originId); - QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; - if (QFile::exists(filePath)) { - // it's very possible the file doesn't exist, because it's inside an archive. we don't support that - QWidget *wid = m_PluginContainer->previewGenerator().genPreview(filePath); - if (wid == nullptr) { - reportError(tr("failed to generate preview for %1").arg(filePath)); - } - else { - preview.addVariant(ToQString(origin.getName()), wid); - } - } - }; - - addFunc(file->getOrigin()); - for (auto alt : file->getAlternatives()) { - addFunc(alt.first); - } - if (preview.numVariants() > 0) { - preview.exec(); - } - else { - QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); - } -} - - -void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos) -{ - m_ConflictsContextItem = ui->overwriteTree->itemAt(pos.x(), pos.y()); - - if (m_ConflictsContextItem != nullptr) { - // offer to hide/unhide file, but not for files from archives - if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { - QMenu menu; - if (m_ConflictsContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(tr("Un-Hide"), this, SLOT(unhideConflictFile())); - } else { - menu.addAction(tr("Hide"), this, SLOT(hideConflictFile())); - } - - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - - QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); - if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { - menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); - } - - menu.exec(ui->overwriteTree->mapToGlobal(pos)); - } - } -} - -void ModInfoDialog::on_overwrittenTree_customContextMenuRequested(const QPoint &pos) -{ - m_ConflictsContextItem = ui->overwrittenTree->itemAt(pos.x(), pos.y()); - - if (m_ConflictsContextItem != nullptr) { - if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { - QMenu menu; - - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - - QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); - if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { - menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); - } - - menu.exec(ui->overwrittenTree->mapToGlobal(pos)); - } - } -} - - -void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int) -{ - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); - this->accept(); -} - -void ModInfoDialog::on_refreshButton_clicked() -{ - m_ModInfo->updateNXMInfo(); - - MessageDialog::showMessage(tr("Info requested, please wait"), this); -} - -void ModInfoDialog::on_endorseBtn_clicked() -{ - emit endorseMod(m_ModInfo); -} - -void ModInfoDialog::on_nextButton_clicked() -{ - int currentTab = ui->tabWidget->currentIndex(); - int tab = m_RealTabPos[currentTab]; - - emit modOpenNext(tab); - this->accept(); -} - -void ModInfoDialog::on_prevButton_clicked() -{ - int currentTab = ui->tabWidget->currentIndex(); - int tab = m_RealTabPos[currentTab]; - - emit modOpenPrev(tab); - this->accept(); -} - - -void ModInfoDialog::createTweak() -{ - QString name = QInputDialog::getText(this, tr("Name"), tr("Please enter a name")); - if (name.isNull()) { - return; - } else if (!fixDirectoryName(name)) { - QMessageBox::critical(this, tr("Error"), tr("Invalid name. Must be a valid file name")); - return; - } else if (ui->iniTweaksList->findItems(name, Qt::MatchFixedString).count() != 0) { - QMessageBox::critical(this, tr("Error"), tr("A tweak by that name exists")); - return; - } - - QListWidgetItem *newTweak = new QListWidgetItem(name + ".ini"); - newTweak->setData(Qt::UserRole, "INI Tweaks/" + name + ".ini"); - newTweak->setFlags(newTweak->flags() | Qt::ItemIsUserCheckable); - newTweak->setCheckState(Qt::Unchecked); - ui->iniTweaksList->addItem(newTweak); -} - -void ModInfoDialog::on_iniTweaksList_customContextMenuRequested(const QPoint &pos) -{ - QMenu menu; - menu.addAction(tr("Create Tweak"), this, SLOT(createTweak())); - menu.exec(ui->iniTweaksList->mapToGlobal(pos)); -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "modinfodialog.h" +#include "ui_modinfodialog.h" +#include "descriptionpage.h" +#include "mainwindow.h" + +#include "modidlineedit.h" +#include "iplugingame.h" +#include "nexusinterface.h" +#include "report.h" +#include "utility.h" +#include "messagedialog.h" +#include "bbcode.h" +#include "questionboxmemory.h" +#include "settings.h" +#include "categories.h" +#include "organizercore.h" +#include "pluginlistsortproxy.h" +#include "previewgenerator.h" +#include "previewdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + + +using namespace MOBase; +using namespace MOShared; + + +class ModFileListWidget : public QListWidgetItem { + friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS); +public: + ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0) + : QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {} +private: + int m_SortValue; +}; + + +static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS) +{ + return LHS.m_SortValue < RHS.m_SortValue; +} + + +ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent) + : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo), + m_ThumbnailMapper(this), m_RequestStarted(false), + m_DeleteAction(nullptr), m_RenameAction(nullptr), m_OpenAction(nullptr), + m_Directory(directory), m_Origin(nullptr), + m_OrganizerCore(organizerCore), m_PluginContainer(pluginContainer) +{ + ui->setupUi(this); + this->setWindowTitle(modInfo->name()); + this->setWindowModality(Qt::WindowModal); + + m_RootPath = modInfo->absolutePath(); + + QString metaFileName = m_RootPath.mid(0).append("/meta.ini"); + m_Settings = new QSettings(metaFileName, QSettings::IniFormat); + + QLineEdit *modIDEdit = findChild("modIDEdit"); + ui->modIDEdit->setValidator(new QIntValidator(modIDEdit)); + ui->modIDEdit->setText(QString("%1").arg(modInfo->getNexusID())); + + connect(ui->modIDEdit, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); + + QString gameName = modInfo->getGameName(); + ui->sourceGameEdit->addItem(organizerCore->managedGame()->gameName(), organizerCore->managedGame()->gameShortName()); + if (organizerCore->managedGame()->validShortNames().size() == 0) { + ui->sourceGameEdit->setDisabled(true); + } else { + for (auto game : pluginContainer->plugins()) { + for (QString gameName : organizerCore->managedGame()->validShortNames()) { + if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + ui->sourceGameEdit->addItem(game->gameName(), game->gameShortName()); + break; + } + } + } + } + ui->sourceGameEdit->setCurrentIndex(ui->sourceGameEdit->findData(gameName)); + + ui->commentsEdit->setText(modInfo->comments()); + ui->notesEdit->setText(modInfo->notes()); + + ui->descriptionView->setPage(new DescriptionPage()); + + connect(&m_ThumbnailMapper, SIGNAL(mapped(const QString&)), this, SIGNAL(thumbnailClickedSignal(const QString&))); + connect(this, SIGNAL(thumbnailClickedSignal(const QString&)), this, SLOT(thumbnailClicked(const QString&))); + connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool))); + connect(ui->descriptionView->page(), SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl))); + //TODO: No easy way to delegate links + //ui->descriptionView->page()->acceptNavigationRequest(QWebEnginePage::DelegateAllLinks); + + new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated())); + + if (directory->originExists(ToWString(modInfo->name()))) { + m_Origin = &directory->getOriginByName(ToWString(modInfo->name())); + if (m_Origin->isDisabled()) { + m_Origin = nullptr; + } + } + + refreshLists(); + + if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) + { + ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); + ui->tabWidget->setTabEnabled(TAB_INIFILES, false); + ui->tabWidget->setTabEnabled(TAB_IMAGES, false); + ui->tabWidget->setTabEnabled(TAB_ESPS, false); + ui->tabWidget->setTabEnabled(TAB_CONFLICTS, false); + //ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); + addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); + refreshPrimaryCategoriesBox(); + ui->tabWidget->setTabEnabled(TAB_NEXUS, false); + //ui->tabWidget->setTabEnabled(TAB_NOTES, false); + ui->tabWidget->setTabEnabled(TAB_FILETREE, false); + } + else if (unmanaged) + { + ui->tabWidget->setTabEnabled(TAB_INIFILES, false); + ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); + ui->tabWidget->setTabEnabled(TAB_NEXUS, false); + ui->tabWidget->setTabEnabled(TAB_FILETREE, false); + ui->tabWidget->setTabEnabled(TAB_NOTES, false); + ui->tabWidget->setTabEnabled(TAB_ESPS, false); + ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); + ui->tabWidget->setTabEnabled(TAB_IMAGES, false); + } else { + initFiletree(modInfo); + addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); + refreshPrimaryCategoriesBox(); + ui->tabWidget->setTabEnabled(TAB_TEXTFILES, ui->textFileList->count() != 0); + ui->tabWidget->setTabEnabled(TAB_IMAGES, ui->thumbnailArea->count() != 0); + ui->tabWidget->setTabEnabled(TAB_ESPS, (ui->inactiveESPList->count() != 0) || (ui->activeESPList->count() != 0)); + } + initINITweaks(); + + ui->tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != nullptr); + + + ui->endorseBtn->setVisible(Settings::instance().endorsementIntegration()); + ui->endorseBtn->setEnabled((m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE) || + (m_ModInfo->endorsedState() == ModInfo::ENDORSED_NEVER)); + + // activate first enabled tab + for (int i = 0; i < ui->tabWidget->count(); ++i) { + if (ui->tabWidget->isTabEnabled(i)) { + ui->tabWidget->setCurrentIndex(i); + break; + } + } + + if (ui->tabWidget->currentIndex() == TAB_NEXUS) { + activateNexusTab(); + } +} + + +ModInfoDialog::~ModInfoDialog() +{ + m_ModInfo->setComments(ui->commentsEdit->text()); + //Avoid saving html stump if notes field is empty. + if (ui->notesEdit->toPlainText().isEmpty()) + m_ModInfo->setNotes(ui->notesEdit->toPlainText()); + else + m_ModInfo->setNotes(ui->notesEdit->toHtml()); + saveCategories(ui->categoriesTree->invisibleRootItem()); + saveIniTweaks(); // ini tweaks are written to the ini file directly. This is the only information not managed by ModInfo + delete ui->descriptionView->page(); + delete ui->descriptionView; + delete ui; + delete m_Settings; +} + + +void ModInfoDialog::initINITweaks() +{ + int numTweaks = m_Settings->beginReadArray("INI Tweaks"); + for (int i = 0; i < numTweaks; ++i) { + m_Settings->setArrayIndex(i); + QList items = ui->iniTweaksList->findItems(m_Settings->value("name").toString(), Qt::MatchFixedString); + if (items.size() != 0) { + items.at(0)->setCheckState(Qt::Checked); + } + } + m_Settings->endArray(); +} + +void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo) +{ + ui->fileTree = findChild("fileTree"); + + m_FileSystemModel = new QFileSystemModel(this); + m_FileSystemModel->setReadOnly(false); + m_FileSystemModel->setRootPath(m_RootPath); + ui->fileTree->setModel(m_FileSystemModel); + ui->fileTree->setRootIndex(m_FileSystemModel->index(m_RootPath)); + ui->fileTree->setColumnWidth(0, 300); + + m_DeleteAction = new QAction(tr("&Delete"), ui->fileTree); + m_RenameAction = new QAction(tr("&Rename"), ui->fileTree); + m_HideAction = new QAction(tr("&Hide"), ui->fileTree); + m_UnhideAction = new QAction(tr("&Unhide"), ui->fileTree); + m_OpenAction = new QAction(tr("&Open"), ui->fileTree); + m_NewFolderAction = new QAction(tr("&New Folder"), ui->fileTree); + QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered())); + QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered())); + QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered())); + QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered())); + QObject::connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered())); + connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered())); +} + + +int ModInfoDialog::tabIndex(const QString &tabId) +{ + for (int i = 0; i < ui->tabWidget->count(); ++i) { + if (ui->tabWidget->widget(i)->objectName() == tabId) { + return i; + } + } + return -1; +} + + +void ModInfoDialog::restoreTabState(const QByteArray &state) +{ + QDataStream stream(state); + int count = 0; + stream >> count; + + QStringList tabIds; + + // first, only determine the new mapping + for (int newPos = 0; newPos < count; ++newPos) { + QString tabId; + stream >> tabId; + tabIds.append(tabId); + int oldPos = tabIndex(tabId); + if (oldPos != -1) { + m_RealTabPos[newPos] = oldPos; + } else { + m_RealTabPos[newPos] = newPos; + } + } + // then actually move the tabs + QTabBar *tabBar = ui->tabWidget->findChild("qt_tabwidget_tabbar"); // magic name = bad + ui->tabWidget->blockSignals(true); + for (int newPos = 0; newPos < count; ++newPos) { + QString tabId = tabIds.at(newPos); + int oldPos = tabIndex(tabId); + tabBar->moveTab(oldPos, newPos); + } + ui->tabWidget->blockSignals(false); +} + + +QByteArray ModInfoDialog::saveTabState() const +{ + QByteArray result; + QDataStream stream(&result, QIODevice::WriteOnly); + stream << ui->tabWidget->count(); + for (int i = 0; i < ui->tabWidget->count(); ++i) { + stream << ui->tabWidget->widget(i)->objectName(); + } + + return result; +} + + +void ModInfoDialog::refreshLists() +{ + int numNonConflicting = 0; + int numOverwrite = 0; + int numOverwritten = 0; + + ui->overwriteTree->clear(); + ui->overwrittenTree->clear(); + + if (m_Origin != nullptr) { + std::vector files = m_Origin->getFiles(); + for (auto iter = files.begin(); iter != files.end(); ++iter) { + QString relativeName = QDir::fromNativeSeparators(ToQString((*iter)->getRelativePath())); + QString fileName = relativeName.mid(0).prepend(m_RootPath); + bool archive; + if ((*iter)->getOrigin(archive) == m_Origin->getID()) { + std::vector>> alternatives = (*iter)->getAlternatives(); + if (!alternatives.empty()) { + std::wostringstream altString; + for (std::vector>>::iterator altIter = alternatives.begin(); + altIter != alternatives.end(); ++altIter) { + if (altIter != alternatives.begin()) { + altString << ", "; + } + altString << m_Directory->getOriginByID(altIter->first).getName(); + } + QStringList fields(relativeName.prepend("...")); + fields.append(ToQString(altString.str())); + + QTreeWidgetItem *item = new QTreeWidgetItem(fields); + item->setData(0, Qt::UserRole, fileName); + item->setData(1, Qt::UserRole, ToQString(m_Directory->getOriginByID(alternatives.back().first).getName())); + item->setData(1, Qt::UserRole + 1, alternatives.back().first); + item->setData(1, Qt::UserRole + 2, archive); + if (archive) { + QFont font = item->font(0); + font.setItalic(true); + item->setFont(0, font); + item->setFont(1, font); + } + ui->overwriteTree->addTopLevelItem(item); + ++numOverwrite; + } else {// otherwise don't display the file + ++numNonConflicting; + } + } else { + FilesOrigin &realOrigin = m_Directory->getOriginByID((*iter)->getOrigin(archive)); + QStringList fields(relativeName); + fields.append(ToQString(realOrigin.getName())); + QTreeWidgetItem *item = new QTreeWidgetItem(fields); + item->setData(0, Qt::UserRole, fileName); + item->setData(1, Qt::UserRole, ToQString(realOrigin.getName())); + item->setData(1, Qt::UserRole + 2, archive); + if (archive) { + QFont font = item->font(0); + font.setItalic(true); + item->setFont(0, font); + item->setFont(1, font); + } + ui->overwrittenTree->addTopLevelItem(item); + ++numOverwritten; + } + } + } + + if (m_RootPath.length() > 0) { + QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories); + while (dirIterator.hasNext()) { + QString fileName = dirIterator.next(); + + if (fileName.endsWith(".txt", Qt::CaseInsensitive)) { + ui->textFileList->addItem(fileName.mid(m_RootPath.length() + 1)); + } else if ((fileName.endsWith(".ini", Qt::CaseInsensitive) || fileName.endsWith(".cfg", Qt::CaseInsensitive)) && + !fileName.endsWith("meta.ini")) { + QString namePart = fileName.mid(m_RootPath.length() + 1); + if (namePart.startsWith("INI Tweaks", Qt::CaseInsensitive)) { + QListWidgetItem *newItem = new QListWidgetItem(namePart.mid(11), ui->iniTweaksList); + newItem->setData(Qt::UserRole, namePart); + newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); + newItem->setCheckState(Qt::Unchecked); + ui->iniTweaksList->addItem(newItem); + } else { + ui->iniFileList->addItem(namePart); + } + } else if (fileName.endsWith(".esp", Qt::CaseInsensitive) || + fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive)) { + QString relativePath = fileName.mid(m_RootPath.length() + 1); + if (relativePath.contains('/')) { + QFileInfo fileInfo(fileName); + QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName()); + newItem->setData(Qt::UserRole, relativePath); + ui->inactiveESPList->addItem(newItem); + } else { + ui->activeESPList->addItem(relativePath); + } + } else if ((fileName.endsWith(".png", Qt::CaseInsensitive)) || + (fileName.endsWith(".jpg", Qt::CaseInsensitive))) { + QImage image = QImage(fileName); + if (!image.isNull()) { + if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { + image = image.scaledToWidth(128); + } else { + image = image.scaledToHeight(96); + } + + QPushButton *thumbnailButton = new QPushButton(QPixmap::fromImage(image), ""); + thumbnailButton->setIconSize(QSize(image.width(), image.height())); + connect(thumbnailButton, SIGNAL(clicked()), &m_ThumbnailMapper, SLOT(map())); + m_ThumbnailMapper.setMapping(thumbnailButton, fileName); + ui->thumbnailArea->addWidget(thumbnailButton); + } + } + } + } + + ui->overwriteCount->display(numOverwrite); + ui->overwrittenCount->display(numOverwritten); + ui->noConflictCount->display(numNonConflicting); +} + + +void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel) +{ + for (int i = 0; i < static_cast(factory.numCategories()); ++i) { + if (factory.getParentID(i) != rootLevel) { + continue; + } + int categoryID = factory.getCategoryID(i); + QTreeWidgetItem *newItem + = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); + newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); + newItem->setCheckState(0, enabledCategories.find(categoryID) + != enabledCategories.end() + ? Qt::Checked + : Qt::Unchecked); + newItem->setData(0, Qt::UserRole, categoryID); + if (factory.hasChildren(i)) { + addCategories(factory, enabledCategories, newItem, categoryID); + } + root->addChild(newItem); + } +} + + +void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode) +{ + for (int i = 0; i < currentNode->childCount(); ++i) { + QTreeWidgetItem *childNode = currentNode->child(i); + m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); + saveCategories(childNode); + } +} + + +void ModInfoDialog::on_closeButton_clicked() +{ + if (allowNavigateFromTXT() && allowNavigateFromINI()) { + this->close(); + } +} + + + +QString ModInfoDialog::getModVersion() const +{ + return m_Settings->value("version", "").toString(); +} + + +const int ModInfoDialog::getModID() const +{ + return m_Settings->value("modid", 0).toInt(); +} + +void ModInfoDialog::openTab(int tab) +{ + QTabWidget *tabWidget = findChild("tabWidget"); + if (tabWidget->isTabEnabled(tab)) { + tabWidget->setCurrentIndex(tab); + } +} + +void ModInfoDialog::thumbnailClicked(const QString &fileName) +{ + QLabel *imageLabel = findChild("imageLabel"); + imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + QImage image(fileName); + if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { + image = image.scaledToWidth(imageLabel->geometry().width()); + } else { + image = image.scaledToHeight(imageLabel->geometry().height()); + } + imageLabel->setPixmap(QPixmap::fromImage(image)); +} + +bool ModInfoDialog::allowNavigateFromTXT() +{ + if (ui->saveTXTButton->isEnabled()) { + int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->textFileView->property("currentFile").toString()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Cancel) { + return false; + } else if (res == QMessageBox::Yes) { + saveCurrentTextFile(); + } + } + return true; +} + + +bool ModInfoDialog::allowNavigateFromINI() +{ + if (ui->saveButton->isEnabled()) { + int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->iniFileView->property("currentFile").toString()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Cancel) { + return false; + } else if (res == QMessageBox::Yes) { + saveCurrentIniFile(); + } + } + return true; +} + + +void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->text(); + + QVariant currentFile = ui->textFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromTXT()) { + openTextFile(fullPath); + } else { + ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } +} + + +void ModInfoDialog::openTextFile(const QString &fileName) +{ + QString encoding; + ui->textFileView->setText(MOBase::readFileText(fileName, &encoding)); + ui->textFileView->setProperty("currentFile", fileName); + ui->textFileView->setProperty("encoding", encoding); + ui->saveTXTButton->setEnabled(false); +} + + +void ModInfoDialog::openIniFile(const QString &fileName) +{ + QFile iniFile(fileName); + iniFile.open(QIODevice::ReadOnly); + QByteArray buffer = iniFile.readAll(); + + QTextCodec *codec = QTextCodec::codecForUtfText(buffer, QTextCodec::codecForName("utf-8")); + QTextEdit *iniFileView = findChild("iniFileView"); + iniFileView->setText(codec->toUnicode(buffer)); + iniFileView->setProperty("currentFile", fileName); + iniFileView->setProperty("encoding", codec->name()); + iniFile.close(); + + ui->saveButton->setEnabled(false); +} + + +void ModInfoDialog::saveIniTweaks() +{ + m_Settings->remove("INI Tweaks"); + m_Settings->beginWriteArray("INI Tweaks"); + + int countEnabled = 0; + for (int i = 0; i < ui->iniTweaksList->count(); ++i) { + if (ui->iniTweaksList->item(i)->checkState() == Qt::Checked) { + m_Settings->setArrayIndex(countEnabled++); + m_Settings->setValue("name", ui->iniTweaksList->item(i)->text()); + } + } + m_Settings->endArray(); +} + + +void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->text(); + + QVariant currentFile = ui->iniFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromINI()) { + openIniFile(fullPath); + } else { + ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } +} + + +void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->data(Qt::UserRole).toString(); + + QVariant currentFile = ui->iniFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromINI()) { + openIniFile(fullPath); + } else { + ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } + +} + + +void ModInfoDialog::on_saveButton_clicked() +{ + saveCurrentIniFile(); +} + + +void ModInfoDialog::on_saveTXTButton_clicked() +{ + saveCurrentTextFile(); +} + + +void ModInfoDialog::saveCurrentTextFile() +{ + QVariant fileNameVar = ui->textFileView->property("currentFile"); + QVariant encodingVar = ui->textFileView->property("encoding"); + if (fileNameVar.isValid() && encodingVar.isValid()) { + QString fileName = fileNameVar.toString(); + QFile txtFile(fileName); + txtFile.open(QIODevice::WriteOnly); + txtFile.resize(0); + QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); + QString data = ui->textFileView->toPlainText().replace("\n", "\r\n"); + txtFile.write(codec->fromUnicode(data)); + } else { + reportError("no file selected"); + } + ui->saveTXTButton->setEnabled(false); +} + + +void ModInfoDialog::saveCurrentIniFile() +{ + QVariant fileNameVar = ui->iniFileView->property("currentFile"); + QVariant encodingVar = ui->iniFileView->property("encoding"); + if (fileNameVar.isValid() && !fileNameVar.toString().isEmpty()) { + QString fileName = fileNameVar.toString(); + QDir().mkpath(QFileInfo(fileName).absolutePath()); + QFile txtFile(fileName); + txtFile.open(QIODevice::WriteOnly); + txtFile.resize(0); + QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); + QString data = ui->iniFileView->toPlainText().replace("\n", "\r\n"); + txtFile.write(codec->fromUnicode(data)); + } else { + reportError("no file selected"); + } + ui->saveButton->setEnabled(false); +} + + +void ModInfoDialog::on_iniFileView_textChanged() +{ + QPushButton* saveButton = findChild("saveButton"); + saveButton->setEnabled(true); +} + + +void ModInfoDialog::on_textFileView_textChanged() +{ + ui->saveTXTButton->setEnabled(true); +} + + +void ModInfoDialog::on_activateESP_clicked() +{ + QListWidget *activeESPList = findChild("activeESPList"); + QListWidget *inactiveESPList = findChild("inactiveESPList"); + + int selectedRow = inactiveESPList->currentRow(); + if (selectedRow < 0) { + return; + } + + QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow); + + QDir root(m_RootPath); + bool renamed = false; + + while (root.exists(selectedItem->text())) { + bool okClicked = false; + QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked); + if (!okClicked) { + inactiveESPList->insertItem(selectedRow, selectedItem); + return; + } else if (newName.size() > 0) { + selectedItem->setText(newName); + renamed = true; + } + } + + if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) { + activeESPList->addItem(selectedItem); + if (renamed) { + selectedItem->setData(Qt::UserRole, QVariant()); + } + } else { + inactiveESPList->insertItem(selectedRow, selectedItem); + reportError(tr("failed to move file")); + } +} + + +void ModInfoDialog::on_deactivateESP_clicked() +{ + QListWidget *activeESPList = findChild("activeESPList"); + QListWidget *inactiveESPList = findChild("inactiveESPList"); + + int selectedRow = activeESPList->currentRow(); + if (selectedRow < 0) { + return; + } + + QDir root(m_RootPath); + + QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow); + + // if we moved the file from optional to active in this session, we move the file back to + // where it came from. Otherwise, it is moved to the new folder "optional" + if (selectedItem->data(Qt::UserRole).isNull()) { + selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text()); + if (!root.exists("optional")) { + if (!root.mkdir("optional")) { + reportError(tr("failed to create directory \"optional\"")); + activeESPList->insertItem(selectedRow, selectedItem); + return; + } + } + } + + if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) { + inactiveESPList->addItem(selectedItem); + } else { + activeESPList->insertItem(selectedRow, selectedItem); + } +} + +void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link) +{ + emit linkActivated(link); +} + +void ModInfoDialog::linkClicked(const QUrl &url) +{ + //Ideally we'd ask the mod for the game and the web service then pass the game + //and URL to the web service + if (NexusInterface::instance(m_PluginContainer)->isURLGameRelated(url)) { + + emit linkActivated(url.toString()); + } else { + ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } +} + +void ModInfoDialog::linkClicked(QString url) +{ + emit linkActivated(url); +} + + +void ModInfoDialog::refreshNexusData(int modID) +{ + if ((!m_RequestStarted) && (modID > 0)) { + m_RequestStarted = true; + + m_ModInfo->updateNXMInfo(); + + MessageDialog::showMessage(tr("Info requested, please wait"), this); + } +} + + +/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + QVariantMap result = resultData.toMap(); + + if (!result["description"].isNull()) { + QString descriptionAsHTML = + QString("" + "" + "%1" + "").arg(BBCode::convertToHTML(result["description"].toString())); + +// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); + ui->descriptionView->setHtml(descriptionAsHTML); + } else { + ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); + } + + QLineEdit *versionEdit = findChild("versionEdit"); + QString version = result["version"].toString(); + + if (!version.isEmpty()) { + m_ModInfo->setNewestVersion(version); + + VersionInfo currentVersion(versionEdit->text()); + VersionInfo newestVersion(version); + + QPalette versionColor; + if (currentVersion < newestVersion) { + versionColor.setColor(QPalette::Text, Qt::red); + versionEdit->setToolTip(tr("Current Version: %1").arg(version)); + } else { + versionColor.setColor(QPalette::Text, Qt::green); + versionEdit->setToolTip(tr("No update available")); + } + versionEdit->setPalette(versionColor); + } +}*/ + + +QString ModInfoDialog::getFileCategory(int categoryID) +{ + switch (categoryID) { + case 1: return tr("Main"); + case 2: return tr("Update"); + case 3: return tr("Optional"); + case 4: return tr("Old"); + case 6: return tr("Deleted"); + default: return tr("Unknown"); + } +} + + +void ModInfoDialog::updateVersionColor() +{ +// QPalette versionColor; + if (m_ModInfo->getVersion() != m_ModInfo->getNewestVersion()) { + ui->versionEdit->setStyleSheet("color: red"); +// versionColor.setColor(QPalette::Text, Qt::red); + ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString())); + } else { + ui->versionEdit->setStyleSheet("color: green"); +// versionColor.setColor(QPalette::Text, Qt::green); + ui->versionEdit->setToolTip(tr("No update available")); + } +// ui->versionEdit->setPalette(versionColor); +} + + +void ModInfoDialog::modDetailsUpdated(bool success) +{ + if (success) { + QString nexusDescription = m_ModInfo->getNexusDescription(); + if (!nexusDescription.isEmpty()) { + /* QString input = + "[size=20]sizetest[/size]\r\n" + "[COLOR=yellow]colortest[/COLOR]\r\n" + "[center]centertest[/center]\r\n" + "[quote]quotetest 1[/quote]\r\n" + "[quote=bla]quotetest 2[/quote]\r\n" + "[url]www.skyrimnexus.com[/url]\r\n" + "[url=www.skyrimnexus.com]urltest 2[/url]\r\n" + "[ol]\r\n" + "[li]item 2[/li]" + "[*]item 1\r\n" + "[/ol]\r\n" + "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n" + "[table][tr][th]headertest1[/th]" + "[th]headertest2[/th][/tr]" + "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]" + "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]" + "[email=\"sherb@gmx.net\"]mail me[/email]"; + ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/ + + QString descriptionAsHTML = + QString("" + "" + "%1" + "").arg(BBCode::convertToHTML(nexusDescription)); + + ui->descriptionView->page()->setHtml(descriptionAsHTML); + + // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); + // ui->descriptionView->setHtml(descriptionAsHTML); + } else { + // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); + ui->descriptionView->page()->setHtml(tr("(description incomplete, please visit nexus)")); + } + + updateVersionColor(); + } +} + + +void ModInfoDialog::activateNexusTab() +{ + QLineEdit *modIDEdit = findChild("modIDEdit"); + int modID = modIDEdit->text().toInt(); + if (modID != 0) { + QString nexusLink = NexusInterface::instance(m_PluginContainer)->getModURL(modID, m_ModInfo->getGameName()); + QLabel *visitNexusLabel = findChild("visitNexusLabel"); + visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink)); + visitNexusLabel->setToolTip(nexusLink); + + if (m_ModInfo->getNexusDescription().isEmpty() || + QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { + refreshNexusData(modID); + } else { + this->modDetailsUpdated(true); + } + } + QLineEdit *versionEdit = findChild("versionEdit"); + QString currentVersion = m_Settings->value("version", "0.0").toString(); + versionEdit->setText(currentVersion); + ui->customUrlLineEdit->setText(m_ModInfo->getURL()); +} + + +void ModInfoDialog::on_tabWidget_currentChanged(int index) +{ + if (index == TAB_NEXUS || m_RealTabPos[index] == TAB_NEXUS) { + activateNexusTab(); + } +} + + +void ModInfoDialog::on_modIDEdit_editingFinished() +{ + int oldID = m_Settings->value("modid", 0).toInt(); + int modID = ui->modIDEdit->text().toInt(); + if (oldID != modID){ + m_ModInfo->setNexusID(modID); + + ui->descriptionView->page()->setHtml(""); + if (modID != 0) { + m_RequestStarted = false; + refreshNexusData(modID); + } + } +} + +void ModInfoDialog::on_sourceGameEdit_currentIndexChanged(int) +{ + for (auto game : m_PluginContainer->plugins()) { + if (game->gameName() == ui->sourceGameEdit->currentText()) { + m_ModInfo->setGameName(game->gameShortName()); + return; + } + } +} + +void ModInfoDialog::on_versionEdit_editingFinished() +{ + VersionInfo version(ui->versionEdit->text()); + m_ModInfo->setVersion(version); + updateVersionColor(); +} + +void ModInfoDialog::on_customUrlLineEdit_editingFinished() +{ + m_ModInfo->setURL(ui->customUrlLineEdit->text()); +} + +bool ModInfoDialog::recursiveDelete(const QModelIndex &index) +{ + for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) { + QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index); + if (m_FileSystemModel->isDir(childIndex)) { + if (!recursiveDelete(childIndex)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); + return false; + } + } else { + if (!m_FileSystemModel->remove(childIndex)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); + return false; + } + } + } + if (!m_FileSystemModel->remove(index)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData()); + return false; + } + return true; +} + + +void ModInfoDialog::on_openInExplorerButton_clicked() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void ModInfoDialog::deleteFile(const QModelIndex &index) +{ + + bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index) + : m_FileSystemModel->remove(index); + if (!res) { + QString fileName = m_FileSystemModel->fileName(index); + reportError(tr("Failed to delete %1").arg(fileName)); + } +} + +void ModInfoDialog::delete_activated() +{ + if (ui->fileTree->hasFocus()) { + QItemSelectionModel *selection = ui->fileTree->selectionModel(); + + if (selection->hasSelection() && selection->selectedRows().count() >= 1) { + + if (selection->selectedRows().count() == 0) { + return; + } + else if (selection->selectedRows().count() == 1) { + QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0)); + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + else { + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + foreach(QModelIndex index, selection->selectedRows()) { + deleteFile(index); + } + } + } +} + +void ModInfoDialog::deleteTriggered() +{ + if (m_FileSelection.count() == 0) { + return; + } else if (m_FileSelection.count() == 1) { + QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0)); + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } else { + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + foreach(QModelIndex index, m_FileSelection) { + deleteFile(index); + } +} + + +void ModInfoDialog::renameTriggered() +{ + QModelIndex selection = m_FileSelection.at(0); + QModelIndex index = selection.sibling(selection.row(), 0); + if (!index.isValid() || m_FileSystemModel->isReadOnly()) { + return; + } + + ui->fileTree->edit(index); +} + + +void ModInfoDialog::hideTriggered() +{ + for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); + iter != m_FileSelection.constEnd(); ++iter) { + QString path = m_FileSystemModel->filePath(*iter); + if (!path.endsWith(ModInfo::s_HiddenExt)) { + hideFile(path); + } + } +} + + +void ModInfoDialog::unhideTriggered() +{ + for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); + iter != m_FileSelection.constEnd(); ++iter) { + QString path = m_FileSystemModel->filePath(*iter); + if (path.endsWith(ModInfo::s_HiddenExt)) { + unhideFile(path); + } + } +} + + +void ModInfoDialog::openFile(const QModelIndex &index) +{ + QString fileName = m_FileSystemModel->filePath(index); + + HINSTANCE res = ::ShellExecuteW(nullptr, L"open", ToWString(fileName).c_str(), nullptr, nullptr, SW_SHOW); + if ((unsigned long long)res <= 32) { + qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res); + } +} + + +void ModInfoDialog::openTriggered() +{ + foreach(QModelIndex idx, m_FileSelection) { + openFile(idx); + } +} + +void ModInfoDialog::createDirectoryTriggered() +{ + QModelIndex selection = m_FileSelection.at(0); + + QModelIndex index = m_FileSystemModel->isDir(selection) ? selection + : selection.parent(); + index = index.sibling(index.row(), 0); + + QString name = tr("New Folder"); + QString path = m_FileSystemModel->filePath(index).append("/"); + + QModelIndex existingIndex = m_FileSystemModel->index(path + name); + int suffix = 1; + while (existingIndex.isValid()) { + name = tr("New Folder") + QString::number(suffix++); + existingIndex = m_FileSystemModel->index(path + name); + } + + QModelIndex newIndex = m_FileSystemModel->mkdir(index, name); + if (!newIndex.isValid()) { + reportError(tr("Failed to create \"%1\"").arg(name)); + return; + } + + ui->fileTree->setCurrentIndex(newIndex); + ui->fileTree->edit(newIndex); +} + + +void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos) +{ + QItemSelectionModel *selectionModel = ui->fileTree->selectionModel(); + m_FileSelection = selectionModel->selectedRows(0); + +// m_FileSelection = ui->fileTree->indexAt(pos); + QMenu menu(ui->fileTree); + + menu.addAction(m_NewFolderAction); + + bool hasFiles = false; + + foreach(QModelIndex idx, m_FileSelection) { + if (m_FileSystemModel->fileInfo(idx).isFile()) { + hasFiles = true; + break; + } + } + + if (selectionModel->hasSelection()) { + if (hasFiles) { + menu.addAction(m_OpenAction); + } + menu.addAction(m_RenameAction); + menu.addAction(m_DeleteAction); + if (m_FileSystemModel->fileName(m_FileSelection.at(0)).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(m_UnhideAction); + } else { + menu.addAction(m_HideAction); + } + } else { + m_FileSelection.clear(); + m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0)); + } + menu.exec(ui->fileTree->mapToGlobal(pos)); +} + + +void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int) +{ + QTreeWidgetItem *parent = item->parent(); + while ((parent != nullptr) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { + parent->setCheckState(0, Qt::Checked); + parent = parent->parent(); + } + refreshPrimaryCategoriesBox(); +} + + +void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree) +{ + for (int i = 0; i < tree->childCount(); ++i) { + QTreeWidgetItem *child = tree->child(i); + if (child->checkState(0) == Qt::Checked) { + ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole)); + addCheckedCategories(child); + } + } +} + + +void ModInfoDialog::refreshPrimaryCategoriesBox() +{ + ui->primaryCategoryBox->clear(); + int primaryCategory = m_ModInfo->getPrimaryCategory(); + addCheckedCategories(ui->categoriesTree->invisibleRootItem()); + for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) { + if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) { + ui->primaryCategoryBox->setCurrentIndex(i); + break; + } + } +} + + +void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index) +{ + if (index != -1) { + m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt()); + } +} + + +void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int) +{ + this->close(); + emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); +} + + +bool ModInfoDialog::hideFile(const QString &oldName) +{ + QString newName = oldName + ModInfo::s_HiddenExt; + + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return false; + } + } else { + return false; + } + } + + if (QFile::rename(oldName, newName)) { + return true; + } else { + reportError(tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName))); + return false; + } +} + + +bool ModInfoDialog::unhideFile(const QString &oldName) +{ + QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return false; + } + } else { + return false; + } + } + if (QFile::rename(oldName, newName)) { + return true; + } else { + reportError(tr("failed to rename %1 to %2").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); + return false; + } +} + + +void ModInfoDialog::hideConflictFile() +{ + if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { + emit originModified(m_Origin->getID()); + refreshLists(); + } +} + + +void ModInfoDialog::unhideConflictFile() +{ + if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { + emit originModified(m_Origin->getID()); + refreshLists(); + } +} + +int ModInfoDialog::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) +{ + QString extension = targetInfo.suffix(); + if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || + (extension.compare("com", Qt::CaseInsensitive) == 0) || + (extension.compare("bat", Qt::CaseInsensitive) == 0)) { + binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); + arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + return 1; + } + else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { + binaryInfo = targetInfo; + return 1; + } + else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { + // types that need to be injected into + std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); + QString binaryPath; + + { // try to find java automatically + WCHAR buffer[MAX_PATH]; + if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { + DWORD binaryType = 0UL; + if (!::GetBinaryTypeW(buffer, &binaryType)) { + qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); + } + else if (binaryType == SCS_32BIT_BINARY) { + binaryPath = ToQString(buffer); + } + } + } + if (binaryPath.isEmpty() && (extension == "jar")) { + // second attempt: look to the registry + QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); + if (javaReg.contains("CurrentVersion")) { + QString currentVersion = javaReg.value("CurrentVersion").toString(); + binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); + } + } + if (binaryPath.isEmpty()) { + binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); + } + if (binaryPath.isEmpty()) { + return 0; + } + binaryInfo = QFileInfo(binaryPath); + if (extension == "jar") { + arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } + else { + arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } + return 1; + } + else { + return 2; + } +} + +void ModInfoDialog::openDataFile() +{ + if (m_ConflictsContextItem != nullptr) { + QFileInfo targetInfo(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); + QFileInfo binaryInfo; + QString arguments; + switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { + case 1: { + m_OrganizerCore->spawnBinaryDirect( + binaryInfo, arguments, m_OrganizerCore->currentProfile()->name(), + targetInfo.absolutePath(), "", ""); + } break; + case 2: { + ::ShellExecuteW(nullptr, L"open", + ToWString(targetInfo.absoluteFilePath()).c_str(), + nullptr, nullptr, SW_SHOWNORMAL); + } break; + default: { + // nop + } break; + } + } +} + +void ModInfoDialog::previewDataFile() +{ + QString fileName = QDir::fromNativeSeparators(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); + + // what we have is an absolute path to the file in its actual location (for the primary origin) + // what we want is the path relative to the virtual data directory + + // we need to look in the virtual directory for the file to make sure the info is up to date. + + // check if the file comes from the actual data folder instead of a mod + QDir gameDirectory = m_OrganizerCore->managedGame()->dataDirectory().absolutePath(); + QString relativePath = gameDirectory.relativeFilePath(fileName); + QDir direRelativePath = gameDirectory.relativeFilePath(fileName); + // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case + if (!direRelativePath.isAbsolute() && !relativePath.startsWith("..")) { + fileName = relativePath; + } + else { + // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory + int offset = m_OrganizerCore->settings().getModDirectory().size() + 1; + offset = fileName.indexOf("/", offset); + fileName = fileName.mid(offset + 1); + } + + + + const FileEntry::Ptr file = m_OrganizerCore->directoryStructure()->searchFile(ToWString(fileName), nullptr); + + if (file.get() == nullptr) { + reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); + return; + } + + // set up preview dialog + PreviewDialog preview(fileName); + auto addFunc = [&](int originId) { + FilesOrigin &origin = m_OrganizerCore->directoryStructure()->getOriginByID(originId); + QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; + if (QFile::exists(filePath)) { + // it's very possible the file doesn't exist, because it's inside an archive. we don't support that + QWidget *wid = m_PluginContainer->previewGenerator().genPreview(filePath); + if (wid == nullptr) { + reportError(tr("failed to generate preview for %1").arg(filePath)); + } + else { + preview.addVariant(ToQString(origin.getName()), wid); + } + } + }; + + addFunc(file->getOrigin()); + for (auto alt : file->getAlternatives()) { + addFunc(alt.first); + } + if (preview.numVariants() > 0) { + preview.exec(); + } + else { + QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); + } +} + + +void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos) +{ + m_ConflictsContextItem = ui->overwriteTree->itemAt(pos.x(), pos.y()); + + if (m_ConflictsContextItem != nullptr) { + // offer to hide/unhide file, but not for files from archives + if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { + QMenu menu; + if (m_ConflictsContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(tr("Un-Hide"), this, SLOT(unhideConflictFile())); + } else { + menu.addAction(tr("Hide"), this, SLOT(hideConflictFile())); + } + + menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); + + QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); + if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { + menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); + } + + menu.exec(ui->overwriteTree->mapToGlobal(pos)); + } + } +} + +void ModInfoDialog::on_overwrittenTree_customContextMenuRequested(const QPoint &pos) +{ + m_ConflictsContextItem = ui->overwrittenTree->itemAt(pos.x(), pos.y()); + + if (m_ConflictsContextItem != nullptr) { + if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { + QMenu menu; + + menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); + + QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); + if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { + menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); + } + + menu.exec(ui->overwrittenTree->mapToGlobal(pos)); + } + } +} + + +void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int) +{ + emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); + this->accept(); +} + +void ModInfoDialog::on_refreshButton_clicked() +{ + m_ModInfo->updateNXMInfo(); + + MessageDialog::showMessage(tr("Info requested, please wait"), this); +} + +void ModInfoDialog::on_endorseBtn_clicked() +{ + emit endorseMod(m_ModInfo); +} + +void ModInfoDialog::on_nextButton_clicked() +{ + int currentTab = ui->tabWidget->currentIndex(); + int tab = m_RealTabPos[currentTab]; + + emit modOpenNext(tab); + this->accept(); +} + +void ModInfoDialog::on_prevButton_clicked() +{ + int currentTab = ui->tabWidget->currentIndex(); + int tab = m_RealTabPos[currentTab]; + + emit modOpenPrev(tab); + this->accept(); +} + + +void ModInfoDialog::createTweak() +{ + QString name = QInputDialog::getText(this, tr("Name"), tr("Please enter a name")); + if (name.isNull()) { + return; + } else if (!fixDirectoryName(name)) { + QMessageBox::critical(this, tr("Error"), tr("Invalid name. Must be a valid file name")); + return; + } else if (ui->iniTweaksList->findItems(name, Qt::MatchFixedString).count() != 0) { + QMessageBox::critical(this, tr("Error"), tr("A tweak by that name exists")); + return; + } + + QListWidgetItem *newTweak = new QListWidgetItem(name + ".ini"); + newTweak->setData(Qt::UserRole, "INI Tweaks/" + name + ".ini"); + newTweak->setFlags(newTweak->flags() | Qt::ItemIsUserCheckable); + newTweak->setCheckState(Qt::Unchecked); + ui->iniTweaksList->addItem(newTweak); +} + +void ModInfoDialog::on_iniTweaksList_customContextMenuRequested(const QPoint &pos) +{ + QMenu menu; + menu.addAction(tr("Create Tweak"), this, SLOT(createTweak())); + menu.exec(ui->iniTweaksList->mapToGlobal(pos)); +} diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 45fe7689..20bfab2a 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -31,6 +31,7 @@ public: virtual void setNeverEndorse() {} virtual bool remove() { return false; } virtual void endorse(bool) {} + virtual void parseNexusInfo() {} virtual bool isEmpty() const { return false; } virtual QString name() const; virtual QString internalName() const { return name(); } diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index cfd662af..b68fb15b 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -33,6 +33,7 @@ public: virtual void setNeverEndorse() {} virtual bool remove() { return false; } virtual void endorse(bool) {} + virtual void parseNexusInfo() {} virtual bool alwaysEnabled() const { return true; } virtual bool isEmpty() const; virtual QString name() const { return "Overwrite"; } diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 3523feae..71b99e10 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -56,7 +56,7 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa connect(&m_NexusBridge, SIGNAL(endorsementToggled(QString,int,QVariant,QVariant)) , this, SLOT(nxmEndorsementToggled(QString,int,QVariant,QVariant))); connect(&m_NexusBridge, SIGNAL(requestFailed(QString,int,int,QVariant,QString)) - , this, SLOT(nxmRequestFailed(QString,int,int,QVariant,QString))); + , this, SLOT(nxmRequestFailed(QString,int,int,QVariant, QNetworkReply::NetworkError,QString))); } @@ -213,13 +213,17 @@ bool ModInfoRegular::downgradeAvailable() const void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant resultData) { QVariantMap result = resultData.toMap(); - setNewestVersion(VersionInfo(result["version"].toString())); setNexusDescription(result["description"].toString()); if ((m_EndorsedState != ENDORSED_NEVER) && (result.contains("endorsement"))) { QVariantMap endorsement = result["endorsement"].toMap(); QString endorsementStatus = endorsement["endorse_status"].toString(); - setEndorsedState(endorsementStatus.compare("Endorsed") == 0 ? ENDORSED_TRUE : ENDORSED_FALSE); + if (endorsementStatus.compare("Endorsed") == 00) + setEndorsedState(ENDORSED_TRUE); + else if (endorsementStatus.compare("Abstained") == 00) + setEndorsedState(ENDORSED_NEVER); + else + setEndorsedState(ENDORSED_FALSE); } m_LastNexusQuery = QDateTime::currentDateTime(); //m_MetaInfoChanged = true; @@ -234,6 +238,8 @@ void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resu if (results["code"].toInt() == 200 || results["code"].toInt() == 201) { if (results["status"].toString().compare("Endorsed") == 0) { m_EndorsedState = ENDORSED_TRUE; + } else if (results["status"].toString().compare("Abstained") == 0) { + m_EndorsedState = ENDORSED_NEVER; } else { m_EndorsedState = ENDORSED_FALSE; } @@ -244,7 +250,7 @@ void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resu } -void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, const QString &errorMessage) +void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage) { QString fullMessage = errorMessage; if (userData.canConvert() && (userData.toInt() == 1)) { diff --git a/src/modinforegular.h b/src/modinforegular.h index 093b4e5b..fdb0e672 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -353,7 +353,7 @@ private slots: void nxmDescriptionAvailable(QString, int modID, QVariant userData, QVariant resultData); void nxmEndorsementToggled(QString, int, QVariant userData, QVariant resultData); - void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, const QString &errorMessage); + void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage); protected: diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index f215fb17..c5e0f0e5 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -20,21 +20,15 @@ public: virtual int getNexusID() const { return -1; } - virtual void setGameName(QString /*gameName*/) - { - } + virtual void setGameName(QString /*gameName*/) {} - virtual void setNexusID(int /*modID*/) - { - } + virtual void setNexusID(int /*modID*/) {} - virtual void endorse(bool /*doEndorse*/) - { - } + virtual void endorse(bool /*doEndorse*/) {} - virtual void ignoreUpdate(bool /*ignore*/) - { - } + virtual void parseNexusInfo() {} + + virtual void ignoreUpdate(bool /*ignore*/) {} virtual bool canBeUpdated() const { return false; } virtual bool canBeEnabled() const { return false; } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 4fe86136..789d30d2 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -1,734 +1,710 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "nexusinterface.h" - -#include "iplugingame.h" -#include "nxmaccessmanager.h" -#include "json.h" -#include "selectiondialog.h" -#include -#include - -#include -#include -#include - -#include - - -using namespace MOBase; -using namespace MOShared; - - -NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule) - : m_Interface(NexusInterface::instance(pluginContainer)) - , m_SubModule(subModule) -{ -} - -void NexusBridge::requestDescription(QString gameName, int modID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestDescription(gameName, modID, this, userData, m_SubModule)); -} - -void NexusBridge::requestFiles(QString gameName, int modID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestFiles(gameName, modID, this, userData, m_SubModule)); -} - -void NexusBridge::requestFileInfo(QString gameName, int modID, int fileID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestFileInfo(gameName, modID, fileID, this, userData, m_SubModule)); -} - -void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule)); -} - -void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule)); -} - -void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - - emit descriptionAvailable(gameName, modID, userData, resultData); - } -} - -void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - - QList fileInfoList; - - QVariantMap resultInfo = resultData.toMap(); - QList resultList = resultInfo["files"].toList(); - - for (const QVariant &file : resultList) { - ModRepositoryFileInfo temp; - QVariantMap fileInfo = file.toMap(); - temp.uri = fileInfo["file_name"].toString(); - temp.name = fileInfo["name"].toString(); - temp.description = fileInfo["changelog_html"].toString(); - temp.version = VersionInfo(fileInfo["version"].toString()); - temp.categoryID = fileInfo["category_id"].toInt(); - temp.fileID = fileInfo["file_id"].toInt(); - temp.fileSize = fileInfo["size"].toInt(); - fileInfoList.append(temp); - } - - emit filesAvailable(gameName, modID, userData, fileInfoList); - } -} - -void NexusBridge::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit fileInfoAvailable(gameName, modID, fileID, userData, resultData); - } -} - -void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit downloadURLsAvailable(gameName, modID, fileID, userData, resultData); - } -} - -void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit endorsementToggled(gameName, modID, userData, resultData); - } -} - -void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorMessage) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit requestFailed(gameName, modID, fileID, userData, errorMessage); - } -} - - -QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); - - -NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_NMMVersion(), m_PluginContainer(pluginContainer) -{ - m_MOVersion = createVersionInfo(); - - m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); - m_DiskCache = new QNetworkDiskCache(this); - connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); -} - -NXMAccessManager *NexusInterface::getAccessManager() -{ - return m_AccessManager; -} - -NexusInterface::~NexusInterface() -{ - cleanup(); -} - -NexusInterface *NexusInterface::instance(PluginContainer *pluginContainer) -{ - static NexusInterface s_Instance(pluginContainer); - return &s_Instance; -} - -void NexusInterface::setCacheDirectory(const QString &directory) -{ - m_DiskCache->setCacheDirectory(directory); - m_AccessManager->setCache(m_DiskCache); -} - -void NexusInterface::setNMMVersion(const QString &nmmVersion) -{ - m_NMMVersion = nmmVersion; - m_AccessManager->setNMMVersion(nmmVersion); -} - -void NexusInterface::loginCompleted() -{ - nextRequest(); -} - - -void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) -{ - //Look for something along the lines of modulename-Vn-m + any old rubbish. - static std::regex exp(R"exp(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\.(zip|rar|7z))exp"); - static std::regex simpleexp("^([a-zA-Z0-9_]+)"); - - QByteArray fileNameUTF8 = fileName.toUtf8(); - std::cmatch result; - if (std::regex_search(fileNameUTF8.constData(), result, exp)) { - modName = QString::fromUtf8(result[1].str().c_str()); - modName = modName.replace('_', ' ').trimmed(); - - std::string candidate = result[3].str(); - std::string candidate2 = result[2].str(); - if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) { - // well, that second match might be an id too... - size_t offset = strspn(candidate2.c_str(), "-_ "); - if (offset < candidate2.length() && query) { - SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName)); - QString r2Highlight(fileName); - r2Highlight.insert(result.position(2) + result.length(2), "* ") - .insert(result.position(2) + static_cast(offset), " *"); - QString r3Highlight(fileName); - r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *"); - - selection.addChoice(candidate.c_str(), r3Highlight, static_cast(strtol(candidate.c_str(), nullptr, 10))); - selection.addChoice(candidate2.c_str() + offset, r2Highlight, static_cast(abs(strtol(candidate2.c_str() + offset, nullptr, 10)))); - if (selection.exec() == QDialog::Accepted) { - modID = selection.getChoiceData().toInt(); - } else { - modID = -1; - } - } else { - modID = -1; - } - } else { - modID = strtol(candidate.c_str(), nullptr, 10); - } - qDebug("mod id guessed: %s -> %d", qUtf8Printable(fileName), modID); - } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) { - qDebug("simple expression matched, using name only"); - modName = QString::fromUtf8(result[1].str().c_str()); - modName = modName.replace('_', ' ').trimmed(); - - modID = -1; - } else { - qDebug("no expression matched!"); - modName.clear(); - modID = -1; - } -} - -bool NexusInterface::isURLGameRelated(const QUrl &url) const -{ - QString const name(url.toString()); - return name.startsWith(getGameURL("") + "/") || - name.startsWith(getOldModsURL("") + "/"); -} - -QString NexusInterface::getGameURL(QString gameName) const -{ - IPluginGame *game = getGame(gameName); - return "https://www.nexusmods.com/" + game->gameNexusName().toLower(); -} - -QString NexusInterface::getOldModsURL(QString gameName) const -{ - IPluginGame *game = getGame(gameName); - return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods"; -} - - -QString NexusInterface::getModURL(int modID, QString gameName = "") const -{ - return QString("%1/mods/%2").arg(getGameURL(gameName)).arg(modID); -} - -std::vector> NexusInterface::getGameChoices(const MOBase::IPluginGame *game) -{ - std::vector> choices; - choices.push_back(std::pair(game->gameShortName(), game->gameName())); - for (QString gameName : game->validShortNames()) { - for (auto gamePlugin : m_PluginContainer->plugins()) { - if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { - choices.push_back(std::pair(gamePlugin->gameShortName(), gamePlugin->gameName())); - break; - } - } - } - return choices; -} - -bool NexusInterface::isModURL(int modID, const QString &url) const -{ - if (QUrl(url) == QUrl(getModURL(modID))) { - return true; - } - //Try the alternate (old style) mod name - QString alt = QString("%1/%2").arg(getOldModsURL("")).arg(modID); - return QUrl(alt) == QUrl(url); -} - -void NexusInterface::setPluginContainer(PluginContainer *pluginContainer) -{ - m_PluginContainer = pluginContainer; -} - -int NexusInterface::requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestUpdates(const std::vector &modIDs, QObject *receiver, QVariant userData, - QString gameName, const QString &subModule) -{ - IPluginGame *game = getGame(gameName); - NXMRequestInfo requestInfo(modIDs, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmUpdatesAvailable(std::vector, QVariant, QVariant, int)), - receiver, SLOT(nxmUpdatesAvailable(std::vector, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -void NexusInterface::fakeFiles() -{ - static int id = 42; - - QVariantList result; - QVariantMap fileMap; - fileMap["uri"] = "fakeURI"; - fileMap["name"] = "fakeName"; - fileMap["description"] = "fakeDescription"; - fileMap["version"] = "1.0.0"; - fileMap["category_id"] = "1"; - fileMap["id"] = "1"; - fileMap["size"] = "512"; - result.append(fileMap); - - emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++); -} - - -int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) -{ - IPluginGame *gamePlugin = getGame(gameName); - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), - receiver, SLOT(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), - receiver, SLOT(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString,int,int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(QString,int,int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); - requestInfo.m_Endorse = endorse; - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - -bool NexusInterface::requiresLogin(const NXMRequestInfo &info) -{ - return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT) - || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL); -} - -IPluginGame* NexusInterface::getGame(QString gameName) const -{ - auto gamePlugins = m_PluginContainer->plugins(); - IPluginGame *gamePlugin = qApp->property("managed_game").value(); - for (auto plugin : gamePlugins) { - if (plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { - gamePlugin = plugin; - break; - } - } - return gamePlugin; -} - -void NexusInterface::cleanup() -{ -// delete m_AccessManager; -// delete m_DiskCache; - m_AccessManager = nullptr; - m_DiskCache = nullptr; -} - -void NexusInterface::clearCache() -{ - m_DiskCache->clear(); - m_AccessManager->clearCookies(); -} - -void NexusInterface::nextRequest() -{ - if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) - || m_RequestQueue.isEmpty()) { - return; - } - - if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { - if (!getAccessManager()->validateAttempted()) { - emit needLogin(); - return; - } else if (getAccessManager()->validateWaiting()) { - return; - } - } - - NXMRequestInfo info = m_RequestQueue.dequeue(); - info.m_Timeout = new QTimer(this); - info.m_Timeout->setInterval(60000); - - QJsonObject postObject; - QJsonDocument postData(postObject); - - QString url; - if (!info.m_Reroute) { - bool hasParams = false; - switch (info.m_Type) { - case NXMRequestInfo::TYPE_DESCRIPTION: { - url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); - } break; - case NXMRequestInfo::TYPE_FILES: { - url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); - } break; - case NXMRequestInfo::TYPE_FILEINFO: { - url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); - } break; - case NXMRequestInfo::TYPE_DOWNLOADURL: { - url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); - } break; - case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { - QString endorse = info.m_Endorse ? "endorse" : "abstain"; - url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse); - postObject.insert("Version", info.m_ModVersion); - postData.setObject(postObject); - } break; - case NXMRequestInfo::TYPE_GETUPDATES: { - QString modIDList = VectorJoin(info.m_ModIDList, ","); - modIDList = "[" + modIDList + "]"; - url = QString("%1/Mods/GetUpdates?ModList=%2").arg(info.m_URL).arg(modIDList); - } break; - } - } else { - url = info.m_URL; - } - QNetworkRequest request(url); - request.setRawHeader("apikey", m_AccessManager->apiKey().toUtf8()); - request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule)); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - request.setRawHeader("Protocol-Version", "0.5.5"); - request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8()); - - if (postData.object().isEmpty()) - info.m_Reply = m_AccessManager->get(request); - else - info.m_Reply = m_AccessManager->post(request, postData.toJson()); - - connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); - connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); - connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); - info.m_Timeout->start(); - m_ActiveRequest.push_back(info); -} - - -void NexusInterface::downloadRequestedNXM(const QString &url) -{ - emit requestNXMDownload(url); -} - -void NexusInterface::requestFinished(std::list::iterator iter) -{ - QNetworkReply *reply = iter->m_Reply; - - if (reply->error() != QNetworkReply::NoError) { - qWarning("request failed: %s", reply->errorString().toUtf8().constData()); - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->errorString()); - } else { - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (statusCode == 301) { - // redirect request, return request to queue - iter->m_URL = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString(); - iter->m_Reroute = true; - m_RequestQueue.enqueue(*iter); - //nextRequest(); - return; - } - QByteArray data = reply->readAll(); - if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) { - QString nexusError(reply->rawHeader("NexusErrorInfo")); - if (nexusError.length() == 0) { - nexusError = tr("empty response"); - } - qDebug("nexus error: %s", qUtf8Printable(nexusError)); - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, nexusError); - } else { - bool ok; - QVariant result = QtJson::parse(data, ok); - if (result.isValid() && ok) { - switch (iter->m_Type) { - case NXMRequestInfo::TYPE_DESCRIPTION: { - emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_FILES: { - emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_FILEINFO: { - emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_DOWNLOADURL: { - emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_GETUPDATES: { - emit nxmUpdatesAvailable(iter->m_ModIDList, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { - emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - } - } else { - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, tr("invalid response")); - } - } - } -} - - -void NexusInterface::requestFinished() -{ - QNetworkReply *reply = static_cast(sender()); - for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { - if (iter->m_Reply == reply) { - iter->m_Timeout->stop(); - iter->m_Timeout->deleteLater(); - requestFinished(iter); - iter->m_Reply->deleteLater(); - m_ActiveRequest.erase(iter); - nextRequest(); - return; - } - } -} - - -void NexusInterface::requestError(QNetworkReply::NetworkError) -{ - QNetworkReply *reply = qobject_cast(sender()); - if (reply == nullptr) { - qWarning("invalid sender type"); - return; - } - - qCritical("request (%s) error: %s (%d)", - qUtf8Printable(reply->url().toString()), - qUtf8Printable(reply->errorString()), - reply->error()); -} - - -void NexusInterface::requestTimeout() -{ - QTimer *timer = qobject_cast(sender()); - if (timer == nullptr) { - qWarning("invalid sender type"); - return; - } - for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { - if (iter->m_Timeout == timer) { - // this abort causes a "request failed" which cleans up the rest - iter->m_Reply->abort(); - return; - } - } -} - -namespace { - QString get_management_url(MOBase::IPluginGame const *game) - { - return "https://api.nexusmods.com/v1"; - } -} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game - ) - : m_ModID(modID) - , m_ModVersion("0") - , m_FileID(0) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID - , QString modVersion - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game -) - : m_ModID(modID) - , m_ModVersion(modVersion) - , m_FileID(0) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector modIDList - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game - ) - : m_ModID(-1) - , m_ModVersion("0") - , m_ModIDList(modIDList) - , m_FileID(0) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID - , int fileID - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game -) - : m_ModID(modID) - , m_ModVersion("0") - , m_FileID(fileID) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "nexusinterface.h" + +#include "iplugingame.h" +#include "nxmaccessmanager.h" +#include "json.h" +#include "selectiondialog.h" +#include "bbcode.h" +#include +#include + +#include +#include +#include + +#include + + +using namespace MOBase; +using namespace MOShared; + + +NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule) + : m_Interface(NexusInterface::instance(pluginContainer)) + , m_SubModule(subModule) +{ +} + +void NexusBridge::requestDescription(QString gameName, int modID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestDescription(gameName, modID, this, userData, m_SubModule)); +} + +void NexusBridge::requestFiles(QString gameName, int modID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestFiles(gameName, modID, this, userData, m_SubModule)); +} + +void NexusBridge::requestFileInfo(QString gameName, int modID, int fileID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestFileInfo(gameName, modID, fileID, this, userData, m_SubModule)); +} + +void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule)); +} + +void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule)); +} + +void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + + emit descriptionAvailable(gameName, modID, userData, resultData); + } +} + +void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + + QList fileInfoList; + + QVariantMap resultInfo = resultData.toMap(); + QList resultList = resultInfo["files"].toList(); + + for (const QVariant &file : resultList) { + ModRepositoryFileInfo temp; + QVariantMap fileInfo = file.toMap(); + temp.uri = fileInfo["file_name"].toString(); + temp.name = fileInfo["name"].toString(); + temp.description = BBCode::convertToHTML(fileInfo["changelog_html"].toString()); + temp.version = VersionInfo(fileInfo["version"].toString()); + temp.categoryID = fileInfo["category_id"].toInt(); + temp.fileID = fileInfo["file_id"].toInt(); + temp.fileSize = fileInfo["size"].toInt(); + fileInfoList.append(temp); + } + + emit filesAvailable(gameName, modID, userData, fileInfoList); + } +} + +void NexusBridge::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit fileInfoAvailable(gameName, modID, fileID, userData, resultData); + } +} + +void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit downloadURLsAvailable(gameName, modID, fileID, userData, resultData); + } +} + +void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit endorsementToggled(gameName, modID, userData, resultData); + } +} + +void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit requestFailed(gameName, modID, fileID, userData, errorMessage); + } +} + + +QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); + + +NexusInterface::NexusInterface(PluginContainer *pluginContainer) + : m_NMMVersion(), m_PluginContainer(pluginContainer) +{ + m_MOVersion = createVersionInfo(); + + m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); + m_DiskCache = new QNetworkDiskCache(this); + connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); +} + +NXMAccessManager *NexusInterface::getAccessManager() +{ + return m_AccessManager; +} + +NexusInterface::~NexusInterface() +{ + cleanup(); +} + +NexusInterface *NexusInterface::instance(PluginContainer *pluginContainer) +{ + static NexusInterface s_Instance(pluginContainer); + return &s_Instance; +} + +void NexusInterface::setCacheDirectory(const QString &directory) +{ + m_DiskCache->setCacheDirectory(directory); + m_AccessManager->setCache(m_DiskCache); +} + +void NexusInterface::setNMMVersion(const QString &nmmVersion) +{ + m_NMMVersion = nmmVersion; + m_AccessManager->setNMMVersion(nmmVersion); +} + +void NexusInterface::loginCompleted() +{ + nextRequest(); +} + + +void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) +{ + //Look for something along the lines of modulename-Vn-m + any old rubbish. + static std::regex exp(R"exp(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\.(zip|rar|7z))exp"); + static std::regex simpleexp("^([a-zA-Z0-9_]+)"); + + QByteArray fileNameUTF8 = fileName.toUtf8(); + std::cmatch result; + if (std::regex_search(fileNameUTF8.constData(), result, exp)) { + modName = QString::fromUtf8(result[1].str().c_str()); + modName = modName.replace('_', ' ').trimmed(); + + std::string candidate = result[3].str(); + std::string candidate2 = result[2].str(); + if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) { + // well, that second match might be an id too... + size_t offset = strspn(candidate2.c_str(), "-_ "); + if (offset < candidate2.length() && query) { + SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName)); + QString r2Highlight(fileName); + r2Highlight.insert(result.position(2) + result.length(2), "* ") + .insert(result.position(2) + static_cast(offset), " *"); + QString r3Highlight(fileName); + r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *"); + + selection.addChoice(candidate.c_str(), r3Highlight, static_cast(strtol(candidate.c_str(), nullptr, 10))); + selection.addChoice(candidate2.c_str() + offset, r2Highlight, static_cast(abs(strtol(candidate2.c_str() + offset, nullptr, 10)))); + if (selection.exec() == QDialog::Accepted) { + modID = selection.getChoiceData().toInt(); + } else { + modID = -1; + } + } else { + modID = -1; + } + } else { + modID = strtol(candidate.c_str(), nullptr, 10); + } + qDebug("mod id guessed: %s -> %d", qUtf8Printable(fileName), modID); + } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) { + qDebug("simple expression matched, using name only"); + modName = QString::fromUtf8(result[1].str().c_str()); + modName = modName.replace('_', ' ').trimmed(); + + modID = -1; + } else { + qDebug("no expression matched!"); + modName.clear(); + modID = -1; + } +} + +bool NexusInterface::isURLGameRelated(const QUrl &url) const +{ + QString const name(url.toString()); + return name.startsWith(getGameURL("") + "/") || + name.startsWith(getOldModsURL("") + "/"); +} + +QString NexusInterface::getGameURL(QString gameName) const +{ + IPluginGame *game = getGame(gameName); + return "https://www.nexusmods.com/" + game->gameNexusName().toLower(); +} + +QString NexusInterface::getOldModsURL(QString gameName) const +{ + IPluginGame *game = getGame(gameName); + return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods"; +} + + +QString NexusInterface::getModURL(int modID, QString gameName = "") const +{ + return QString("%1/mods/%2").arg(getGameURL(gameName)).arg(modID); +} + +std::vector> NexusInterface::getGameChoices(const MOBase::IPluginGame *game) +{ + std::vector> choices; + choices.push_back(std::pair(game->gameShortName(), game->gameName())); + for (QString gameName : game->validShortNames()) { + for (auto gamePlugin : m_PluginContainer->plugins()) { + if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + choices.push_back(std::pair(gamePlugin->gameShortName(), gamePlugin->gameName())); + break; + } + } + } + return choices; +} + +bool NexusInterface::isModURL(int modID, const QString &url) const +{ + if (QUrl(url) == QUrl(getModURL(modID))) { + return true; + } + //Try the alternate (old style) mod name + QString alt = QString("%1/%2").arg(getOldModsURL("")).arg(modID); + return QUrl(alt) == QUrl(url); +} + +void NexusInterface::setPluginContainer(PluginContainer *pluginContainer) +{ + m_PluginContainer = pluginContainer; +} + +int NexusInterface::requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, + QString gameName, const QString &subModule) +{ + IPluginGame *game = getGame(gameName); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +void NexusInterface::fakeFiles() +{ + static int id = 42; + + QVariantList result; + QVariantMap fileMap; + fileMap["uri"] = "fakeURI"; + fileMap["name"] = "fakeName"; + fileMap["description"] = "fakeDescription"; + fileMap["version"] = "1.0.0"; + fileMap["category_id"] = "1"; + fileMap["id"] = "1"; + fileMap["size"] = "512"; + result.append(fileMap); + + emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++); +} + + +int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + connect(this, SIGNAL(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) +{ + IPluginGame *gamePlugin = getGame(gameName); + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), + receiver, SLOT(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), + receiver, SLOT(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString,int,int,QVariant,int,QNetworkReply::NetworkError,QString)), + receiver, SLOT(nxmRequestFailed(QString,int,int,QVariant,int,QNetworkReply::NetworkError,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); + requestInfo.m_Endorse = endorse; + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + +bool NexusInterface::requiresLogin(const NXMRequestInfo &info) +{ + return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT) + || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL); +} + +IPluginGame* NexusInterface::getGame(QString gameName) const +{ + auto gamePlugins = m_PluginContainer->plugins(); + IPluginGame *gamePlugin = qApp->property("managed_game").value(); + for (auto plugin : gamePlugins) { + if (plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + gamePlugin = plugin; + break; + } + } + return gamePlugin; +} + +void NexusInterface::cleanup() +{ +// delete m_AccessManager; +// delete m_DiskCache; + m_AccessManager = nullptr; + m_DiskCache = nullptr; +} + +void NexusInterface::clearCache() +{ + m_DiskCache->clear(); + m_AccessManager->clearCookies(); +} + +void NexusInterface::nextRequest() +{ + if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) + || m_RequestQueue.isEmpty()) { + return; + } + + if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { + if (!getAccessManager()->validateAttempted()) { + emit needLogin(); + return; + } else if (getAccessManager()->validateWaiting()) { + return; + } + } + + NXMRequestInfo info = m_RequestQueue.dequeue(); + info.m_Timeout = new QTimer(this); + info.m_Timeout->setInterval(60000); + + QJsonObject postObject; + QJsonDocument postData(postObject); + + QString url; + if (!info.m_Reroute) { + bool hasParams = false; + switch (info.m_Type) { + case NXMRequestInfo::TYPE_DESCRIPTION: { + url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_FILES: { + url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_GETUPDATES: { + url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_FILEINFO: { + url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); + } break; + case NXMRequestInfo::TYPE_DOWNLOADURL: { + url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); + } break; + case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { + QString endorse = info.m_Endorse ? "endorse" : "abstain"; + url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse); + postObject.insert("Version", info.m_ModVersion); + postData.setObject(postObject); + } break; + } + } else { + url = info.m_URL; + } + QNetworkRequest request(url); + request.setRawHeader("apikey", m_AccessManager->apiKey().toUtf8()); + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule)); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Protocol-Version", "0.5.5"); + request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8()); + + if (postData.object().isEmpty()) + info.m_Reply = m_AccessManager->get(request); + else + info.m_Reply = m_AccessManager->post(request, postData.toJson()); + + connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); + connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); + info.m_Timeout->start(); + m_ActiveRequest.push_back(info); +} + + +void NexusInterface::downloadRequestedNXM(const QString &url) +{ + emit requestNXMDownload(url); +} + +void NexusInterface::requestFinished(std::list::iterator iter) +{ + QNetworkReply *reply = iter->m_Reply; + + if (reply->error() != QNetworkReply::NoError) { + qWarning("request failed: %s", reply->errorString().toUtf8().constData()); + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString()); + } else { + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 301) { + // redirect request, return request to queue + iter->m_URL = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString(); + iter->m_Reroute = true; + m_RequestQueue.enqueue(*iter); + //nextRequest(); + return; + } + QByteArray data = reply->readAll(); + if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) { + QString nexusError(reply->rawHeader("NexusErrorInfo")); + if (nexusError.length() == 0) { + nexusError = tr("empty response"); + } + qDebug("nexus error: %s", qUtf8Printable(nexusError)); + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), nexusError); + } else { + bool ok; + QVariant result = QtJson::parse(data, ok); + if (result.isValid() && ok) { + switch (iter->m_Type) { + case NXMRequestInfo::TYPE_DESCRIPTION: { + emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_FILES: { + emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_FILEINFO: { + emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_DOWNLOADURL: { + emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_GETUPDATES: { + emit nxmUpdatesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { + emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + } + } else { + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), tr("invalid response")); + } + } + } +} + + +void NexusInterface::requestFinished() +{ + QNetworkReply *reply = static_cast(sender()); + for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { + if (iter->m_Reply == reply) { + iter->m_Timeout->stop(); + iter->m_Timeout->deleteLater(); + requestFinished(iter); + iter->m_Reply->deleteLater(); + m_ActiveRequest.erase(iter); + nextRequest(); + return; + } + } +} + + +void NexusInterface::requestError(QNetworkReply::NetworkError) +{ + QNetworkReply *reply = qobject_cast(sender()); + if (reply == nullptr) { + qWarning("invalid sender type"); + return; + } + + qCritical("request (%s) error: %s (%d)", + qUtf8Printable(reply->url().toString()), + qUtf8Printable(reply->errorString()), + reply->error()); +} + + +void NexusInterface::requestTimeout() +{ + QTimer *timer = qobject_cast(sender()); + if (timer == nullptr) { + qWarning("invalid sender type"); + return; + } + for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { + if (iter->m_Timeout == timer) { + // this abort causes a "request failed" which cleans up the rest + iter->m_Reply->abort(); + return; + } + } +} + +namespace { + QString get_management_url(MOBase::IPluginGame const *game) + { + return "https://api.nexusmods.com/v1"; + } +} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game + ) + : m_ModID(modID) + , m_ModVersion("0") + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , QString modVersion + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(modID) + , m_ModVersion(modVersion) + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , int fileID + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(modID) + , m_ModVersion("0") + , m_FileID(fileID) + , m_Reply(nullptr) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 7b707711..56cdef47 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -1,424 +1,423 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef NEXUSINTERFACE_H -#define NEXUSINTERFACE_H - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -namespace MOBase { class IPluginGame; } - -class NexusInterface; -class NXMAccessManager; - -/** - * @brief convenience class to make nxm requests easier - * usually, all objects that started a nxm request will be signaled if one finished. - * Therefore, the objects need to store the id of the requests they started and then filter - * the result. - * NexusBridge does this automatically. Users connect to the signals of NexusBridge they intend - * to handle and only receive the signals the caused - **/ -class NexusBridge : public MOBase::IModRepositoryBridge -{ - - Q_OBJECT - -public: - - NexusBridge(PluginContainer *pluginContainer, const QString &subModule = ""); - - /** - * @brief request description for a mod - * - * @param modID id of the mod caller is interested in - * @param userData user data to be returned with the result - * @param url the url to request from - **/ - virtual void requestDescription(QString gameName, int modID, QVariant userData); - - /** - * @brief request a list of the files belonging to a mod - * - * @param modID id of the mod caller is interested in - * @param userData user data to be returned with the result - **/ - virtual void requestFiles(QString gameName, int modID, QVariant userData); - - /** - * @brief request info about a single file of a mod - * - * @param modID id of the mod caller is interested in - * @param fileID id of the file the caller is interested in - * @param userData user data to be returned with the result - **/ - virtual void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData); - - /** - * @brief request the download url of a file - * - * @param modID id of the mod caller is interested in - * @param fileID id of the file the caller is interested in - * @param userData user data to be returned with the result - **/ - virtual void requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData); - - /** - * @brief requestToggleEndorsement - * @param modID id of the mod caller is interested in - * @param userData user data to be returned with the result - */ - virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData); - -public slots: - - void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorMessage); - -private: - - NexusInterface *m_Interface; - QString m_SubModule; - std::set m_RequestIDs; - -}; - - -/** - * @brief Makes asynchronous requests to the nexus API - * - * This class can be used to make asynchronous requests to the Nexus API. - * Currently, responses are sent to all receivers that have sent a request of the relevant type, so the - * recipient has to filter the response by the id returned when making the request - **/ -class NexusInterface : public QObject -{ - Q_OBJECT - -public: - - ~NexusInterface(); - - static NexusInterface *instance(PluginContainer *pluginContainer); - - /** - * @return the access manager object used to connect to nexus - **/ - NXMAccessManager *getAccessManager(); - - /** - * @brief cleanup this interface. this is destructive, afterwards it can't be used again - */ - void cleanup(); - - /** - * @brief clear webcache and cookies associated with this access manager - */ - void clearCache(); - - /** - * @brief request description for a mod - * - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestDescription(gameName, modID, receiver, userData, subModule, getGame(gameName)); - } - - /** - * @brief request description for a mod - * - * @param modID id of the mod caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) - * @param userData user data to be returned with the result - * @param game Game with which the mod is associated - * @return int an id to identify the request - **/ - int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, - MOBase::IPluginGame const *game); - - /** - * @brief request nexus descriptions for multiple mods at once - * @param modIDs a list of ids of mods the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - */ - int requestUpdates(const std::vector &modIDs, QObject *receiver, QVariant userData, QString gameName, const QString &subModule); - - /** - * @brief request a list of the files belonging to a mod - * - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestFiles(gameName, modID, receiver, userData, subModule, getGame(gameName)); - } - - - /** - * @brief request a list of the files belonging to a mod - * - * @param modID id of the mod caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - **/ - int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, - MOBase::IPluginGame const *game); - - /** - * @brief request info about a single file of a mod - * - * @param game name of the game short name to request the download from - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param fileID id of the file the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule); - - /** - * @brief request the download url of a file - * - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param fileID id of the file the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestDownloadURL(gameName, modID, fileID, receiver, userData, subModule, getGame(gameName)); - } - - /** - * @brief request the download url of a file - * - * @param modID id of the mod caller is interested in - * @param fileID id of the file the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - **/ - int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - - /** - * @brief toggle endorsement state of the mod - * @param modID id of the mod (assumed to be for the current game) - * @param endorse true if the mod should be endorsed, false for un-endorse - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - */ - int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestToggleEndorsement(gameName, modID, modVersion, endorse, receiver, userData, subModule, getGame(gameName)); - } - - /** - * @brief toggle endorsement state of the mod - * @param modID id of the mod - * @param endorse true if the mod should be endorsed, false for un-endorse - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - */ - int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, - MOBase::IPluginGame const *game); - - /** - * @param directory the directory to store cache files - **/ - void setCacheDirectory(const QString &directory); - - /** - * MO has to send a "Nexus Client Vx.y.z" as part of the user agent to be allowed to use the API - * @param nmmVersion the version of nmm to impersonate - **/ - void setNMMVersion(const QString &nmmVersion); - - /** - * @brief called when the log-in completes. This was, requests waiting for the log-in can be run - */ - void loginCompleted(); - - std::vector> getGameChoices(const MOBase::IPluginGame *game); - -public: - - /** - * @brief guess the mod id from a filename as delivered by Nexus - * @param fileName name of the file - * @return the guessed mod id - * @note this currently doesn't fit well with the remaining interface but this is the best place for the function - */ - static void interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query); - - /** - * @brief get the currently managed game - */ - MOBase::IPluginGame const *managedGame() const; - - /** - * @brief see if the passed URL is related to the current game - * - * Arguably, this should optionally take a gameplugin pointer - */ - bool isURLGameRelated(QUrl const &url) const; - - /** - * @brief Get the nexus page for the current game - * - * Arguably, this should optionally take a gameplugin pointer - */ - QString getGameURL(QString gameName) const; - - /** - * @brief Get the URL for the mod web page - * @param modID - */ - QString getModURL(int modID, QString gameName) const; - - /** - * @brief Checks if the specified URL might correspond to a nexus mod - * @param modID - * @param url - * @return - */ - bool isModURL(int modID, QString const &url) const; - - void setPluginContainer(PluginContainer *pluginContainer); - -signals: - - void requestNXMDownload(const QString &url); - - void needLogin(); - - void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int requestID); - void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString); - -private slots: - - void requestFinished(); - void requestError(QNetworkReply::NetworkError error); - void requestTimeout(); - - void downloadRequestedNXM(const QString &url); - - void fakeFiles(); - -private: - - struct NXMRequestInfo { - int m_ModID; - QString m_ModVersion; - std::vector m_ModIDList; - int m_FileID; - QNetworkReply *m_Reply; - enum Type { - TYPE_DESCRIPTION, - TYPE_FILES, - TYPE_FILEINFO, - TYPE_DOWNLOADURL, - TYPE_TOGGLEENDORSEMENT, - TYPE_GETUPDATES - } m_Type; - QVariant m_UserData; - QTimer *m_Timeout; - QString m_URL; - QString m_SubModule; - QString m_GameName; - int m_NexusGameID; - bool m_Reroute; - int m_ID; - int m_Endorse; - - NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - NXMRequestInfo(std::vector modIDList, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - - private: - static QAtomicInt s_NextID; - }; - - static const int MAX_ACTIVE_DOWNLOADS = 2; - -private: - - NexusInterface(PluginContainer *pluginContainer); - void nextRequest(); - void requestFinished(std::list::iterator iter); - bool requiresLogin(const NXMRequestInfo &info); - MOBase::IPluginGame *getGame(QString gameName) const; - QString getOldModsURL(QString gameName) const; - -private: - - QNetworkDiskCache *m_DiskCache; - - NXMAccessManager *m_AccessManager; - - std::list m_ActiveRequest; - QQueue m_RequestQueue; - - MOBase::VersionInfo m_MOVersion; - QString m_NMMVersion; - - PluginContainer *m_PluginContainer; - -}; - -#endif // NEXUSINTERFACE_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef NEXUSINTERFACE_H +#define NEXUSINTERFACE_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace MOBase { class IPluginGame; } + +class NexusInterface; +class NXMAccessManager; + +/** + * @brief convenience class to make nxm requests easier + * usually, all objects that started a nxm request will be signaled if one finished. + * Therefore, the objects need to store the id of the requests they started and then filter + * the result. + * NexusBridge does this automatically. Users connect to the signals of NexusBridge they intend + * to handle and only receive the signals the caused + **/ +class NexusBridge : public MOBase::IModRepositoryBridge +{ + + Q_OBJECT + +public: + + NexusBridge(PluginContainer *pluginContainer, const QString &subModule = ""); + + /** + * @brief request description for a mod + * + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + * @param url the url to request from + **/ + virtual void requestDescription(QString gameName, int modID, QVariant userData); + + /** + * @brief request a list of the files belonging to a mod + * + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestFiles(QString gameName, int modID, QVariant userData); + + /** + * @brief request info about a single file of a mod + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData); + + /** + * @brief request the download url of a file + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData); + + /** + * @brief requestToggleEndorsement + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + */ + virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData); + +public slots: + + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage); + +private: + + NexusInterface *m_Interface; + QString m_SubModule; + std::set m_RequestIDs; + +}; + + +/** + * @brief Makes asynchronous requests to the nexus API + * + * This class can be used to make asynchronous requests to the Nexus API. + * Currently, responses are sent to all receivers that have sent a request of the relevant type, so the + * recipient has to filter the response by the id returned when making the request + **/ +class NexusInterface : public QObject +{ + Q_OBJECT + +public: + + ~NexusInterface(); + + static NexusInterface *instance(PluginContainer *pluginContainer); + + /** + * @return the access manager object used to connect to nexus + **/ + NXMAccessManager *getAccessManager(); + + /** + * @brief cleanup this interface. this is destructive, afterwards it can't be used again + */ + void cleanup(); + + /** + * @brief clear webcache and cookies associated with this access manager + */ + void clearCache(); + + /** + * @brief request description for a mod + * + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestDescription(gameName, modID, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @brief request description for a mod + * + * @param modID id of the mod caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) + * @param userData user data to be returned with the result + * @param game Game with which the mod is associated + * @return int an id to identify the request + **/ + int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + + /** + * @brief request nexus descriptions for multiple mods at once + * @param modIDs a list of ids of mods the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule); + + /** + * @brief request a list of the files belonging to a mod + * + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestFiles(gameName, modID, receiver, userData, subModule, getGame(gameName)); + } + + + /** + * @brief request a list of the files belonging to a mod + * + * @param modID id of the mod caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + **/ + int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + + /** + * @brief request info about a single file of a mod + * + * @param game name of the game short name to request the download from + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param fileID id of the file the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule); + + /** + * @brief request the download url of a file + * + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param fileID id of the file the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestDownloadURL(gameName, modID, fileID, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @brief request the download url of a file + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + **/ + int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + + /** + * @brief toggle endorsement state of the mod + * @param modID id of the mod (assumed to be for the current game) + * @param endorse true if the mod should be endorsed, false for un-endorse + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + */ + int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestToggleEndorsement(gameName, modID, modVersion, endorse, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @brief toggle endorsement state of the mod + * @param modID id of the mod + * @param endorse true if the mod should be endorsed, false for un-endorse + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + + /** + * @param directory the directory to store cache files + **/ + void setCacheDirectory(const QString &directory); + + /** + * MO has to send a "Nexus Client Vx.y.z" as part of the user agent to be allowed to use the API + * @param nmmVersion the version of nmm to impersonate + **/ + void setNMMVersion(const QString &nmmVersion); + + /** + * @brief called when the log-in completes. This was, requests waiting for the log-in can be run + */ + void loginCompleted(); + + std::vector> getGameChoices(const MOBase::IPluginGame *game); + +public: + + /** + * @brief guess the mod id from a filename as delivered by Nexus + * @param fileName name of the file + * @return the guessed mod id + * @note this currently doesn't fit well with the remaining interface but this is the best place for the function + */ + static void interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query); + + /** + * @brief get the currently managed game + */ + MOBase::IPluginGame const *managedGame() const; + + /** + * @brief see if the passed URL is related to the current game + * + * Arguably, this should optionally take a gameplugin pointer + */ + bool isURLGameRelated(QUrl const &url) const; + + /** + * @brief Get the nexus page for the current game + * + * Arguably, this should optionally take a gameplugin pointer + */ + QString getGameURL(QString gameName) const; + + /** + * @brief Get the URL for the mod web page + * @param modID + */ + QString getModURL(int modID, QString gameName) const; + + /** + * @brief Checks if the specified URL might correspond to a nexus mod + * @param modID + * @param url + * @return + */ + bool isModURL(int modID, QString const &url) const; + + void setPluginContainer(PluginContainer *pluginContainer); + +signals: + + void requestNXMDownload(const QString &url); + + void needLogin(); + + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); + +private slots: + + void requestFinished(); + void requestError(QNetworkReply::NetworkError error); + void requestTimeout(); + + void downloadRequestedNXM(const QString &url); + + void fakeFiles(); + +private: + + struct NXMRequestInfo { + int m_ModID; + QString m_ModVersion; + std::vector m_ModIDList; + int m_FileID; + QNetworkReply *m_Reply; + enum Type { + TYPE_DESCRIPTION, + TYPE_FILES, + TYPE_FILEINFO, + TYPE_DOWNLOADURL, + TYPE_TOGGLEENDORSEMENT, + TYPE_GETUPDATES + } m_Type; + QVariant m_UserData; + QTimer *m_Timeout; + QString m_URL; + QString m_SubModule; + QString m_GameName; + int m_NexusGameID; + bool m_Reroute; + int m_ID; + int m_Endorse; + + NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + + private: + static QAtomicInt s_NextID; + }; + + static const int MAX_ACTIVE_DOWNLOADS = 2; + +private: + + NexusInterface(PluginContainer *pluginContainer); + void nextRequest(); + void requestFinished(std::list::iterator iter); + bool requiresLogin(const NXMRequestInfo &info); + MOBase::IPluginGame *getGame(QString gameName) const; + QString getOldModsURL(QString gameName) const; + +private: + + QNetworkDiskCache *m_DiskCache; + + NXMAccessManager *m_AccessManager; + + std::list m_ActiveRequest; + QQueue m_RequestQueue; + + MOBase::VersionInfo m_MOVersion; + QString m_NMMVersion; + + PluginContainer *m_PluginContainer; + +}; + +#endif // NEXUSINTERFACE_H diff --git a/src/organizer_en.ts b/src/organizer_en.ts index bcdbd069..0d63c34c 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -571,301 +571,301 @@ p, li { white-space: pre-wrap; } - + Memory allocation error (in refreshing directory). - + failed to download %1: could not open output file: %2 - + Download again? - + A file with the same name "%1" has already been downloaded. Do you want to download it again? The new file will receive a different name. - + Wrong Game - + The download link is for a mod for "%1" but this instance of MO has been set up for "%2". - - + + Already Started - + A download for this mod file has already been queued. - + There is already a download started for this file (mod: %1, file: %2). - - + + remove: invalid download index %1 - + failed to delete %1 - + failed to delete meta file for %1 - + restore: invalid download index: %1 - + cancel: invalid download index %1 - + pause: invalid download index %1 - + resume: invalid download index %1 - + resume (int): invalid download index %1 - + No known download urls. Sorry, this download can't be resumed. - + query: invalid download index %1 - + Please enter the nexus mod id - + Mod ID: - + Please select the source game code for %1 - + VisitNexus: invalid download index %1 - + Nexus ID for this Mod is unknown - + OpenFile: invalid download index %1 - + OpenFileInDownloadsFolder: invalid download index %1 - + get pending: invalid download index %1 - + get path: invalid download index %1 - + Main - + Update - + Optional - + Old - + Misc - + Unknown - + display name: invalid download index %1 - + file name: invalid download index %1 - + file time: invalid download index %1 - + file size: invalid download index %1 - + progress: invalid download index %1 - + state: invalid download index %1 - + infocomplete: invalid download index %1 - - + + mod id: invalid download index %1 - + ishidden: invalid download index %1 - + file info: invalid download index %1 - + mark installed: invalid download index %1 - + mark uninstalled: invalid download index %1 - + Memory allocation error (in processing progress event). - + Memory allocation error (in processing downloaded data). - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. - + failed to re-open %1 - + Unable to write download to drive (return %1). Check the drive's available storage. @@ -2431,7 +2431,7 @@ Please enter a name: - + Are you sure? @@ -2768,13 +2768,13 @@ You can also use online editors and converters instead. - + Enable selected - + Disable selected @@ -2835,13 +2835,13 @@ You can also use online editors and converters instead. - + Exception: - + Unknown exception @@ -2979,7 +2979,7 @@ Click OK to restart MO now. - + Set Priority @@ -3044,206 +3044,206 @@ Click OK to restart MO now. - + Thank you! - + Thank you for your endorsement! - + Okay. - + This mod will not be endorsed and will no longer ask you to endorse. - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - - + + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + This will restart MO, continue? - + Edit Categories... - + Deselect filter - + Remove - + Enable all - + Disable all - + Unlock load order - + Lock load order - + Open Origin in Explorer - + Open Origin Info... - + depends on missing "%1" - + incompatible with "%1" - + Please wait while LOOT is running - + loot failed. Exit code was: %1 - + failed to start loot - + failed to run loot: %1 - + Errors occured - + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of modlist created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file - + Set the priority of the selected mods @@ -3768,7 +3768,7 @@ p, li { white-space: pre-wrap; } - Misc + Deleted @@ -3990,12 +3990,12 @@ p, li { white-space: pre-wrap; } - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> @@ -4350,7 +4350,7 @@ p, li { white-space: pre-wrap; } - timeout + There was a timeout during the request @@ -4370,17 +4370,17 @@ p, li { white-space: pre-wrap; } NexusInterface - + Failed to guess mod id for "%1", please pick the correct one - + empty response - + invalid response @@ -5617,7 +5617,7 @@ If the folder was still in use, restart MO and try again. - + Mod Organizer @@ -5694,12 +5694,12 @@ If the folder was still in use, restart MO and try again. - + Script Extender - + Proxy DLL @@ -5921,28 +5921,28 @@ Select Show Details option to see the full change-log. - - + + Error - + Failed to retrieve a Nexus API key! Please try again.A browser window should open asking you to authorize. - + Failed to create "%1", you may not have the necessary permission. path remains unchanged. - + Restart Mod Organizer? - + In order to reset the window geometries, MO must be restarted. Restart it now? @@ -6215,137 +6215,147 @@ p, li { white-space: pre-wrap; } - - Remove cache and cookies. Forces a new login. + + Clear the stored Nexus API key and force reauthorization. + + + + + Revoke Nexus Authorization - + + Remove cache and cookies. + + + + Clear Cache - + Disable automatic internet features - + Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) - + Offline Mode - + Use a proxy for network connections. - + Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. - + Use HTTP Proxy (Uses System Settings) - + Endorsement Integration - + Associate with "Download with manager" links - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + Username - + Password - + If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure. - + Plugins - + Author: - + Version: - + Description: - + Key - + Value - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -6361,17 +6371,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -6382,17 +6392,17 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - + NMM Version - + The Version of Nexus Mod Manager to impersonate. - + Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in. On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn't need an update. Therefore you can configure the version to identify as here. Please note that MO does identify itself as MO to the webserver, it's not lying about what it is. It is merely adding a "compatible" NMM version to the user agent. @@ -6401,28 +6411,28 @@ tl;dr-version: If Nexus-features don't work, insert the current version num - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -6430,66 +6440,66 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Display mods installed outside MO - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - - + + Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - + Lock GUI when running executable - + Enable parsing of Archives. Has negative effects on performance. - + <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html> - + Enable parsing of Archives - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -6498,48 +6508,48 @@ programs you are intentionally running. - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running. - + Configure Executables Blacklist - - + + Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations. - + Reset Window Geometries - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - + Max Dumps To Keep - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -6547,12 +6557,12 @@ programs you are intentionally running. - + Hint: right click link and copy link location - + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -6562,17 +6572,17 @@ programs you are intentionally running. - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -6583,37 +6593,37 @@ programs you are intentionally running. - + None - + Mini (recommended) - + Data - + Full - + Log Level - + Decides the amount of data printed to "ModOrganizer.log" - + Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. @@ -6621,22 +6631,22 @@ programs you are intentionally running. - + Debug - + Info (recommended) - + Warning - + Error -- cgit v1.3.1 From fbbff08fb8774d8f6a22efe9532dc27ce7a1dae7 Mon Sep 17 00:00:00 2001 From: Silarn Date: Sun, 27 Jan 2019 19:55:15 -0600 Subject: Fix up mismatched signals and slots --- src/modinforegular.cpp | 4 ++-- src/nexusinterface.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 71b99e10..606924e4 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -55,8 +55,8 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa , this, SLOT(nxmDescriptionAvailable(QString,int,QVariant,QVariant))); connect(&m_NexusBridge, SIGNAL(endorsementToggled(QString,int,QVariant,QVariant)) , this, SLOT(nxmEndorsementToggled(QString,int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(requestFailed(QString,int,int,QVariant,QString)) - , this, SLOT(nxmRequestFailed(QString,int,int,QVariant, QNetworkReply::NetworkError,QString))); + connect(&m_NexusBridge, SIGNAL(requestFailed(QString,int,int,QVariant,QNetworkReply::NetworkError,QString)) + , this, SLOT(nxmRequestFailed(QString,int,int,QVariant,QNetworkReply::NetworkError,QString))); } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 789d30d2..19fc3af4 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -139,7 +139,7 @@ void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVar std::set::iterator iter = m_RequestIDs.find(requestID); if (iter != m_RequestIDs.end()) { m_RequestIDs.erase(iter); - emit requestFailed(gameName, modID, fileID, userData, errorMessage); + emit requestFailed(gameName, modID, fileID, userData, error, errorMessage); } } -- cgit v1.3.1 From b6498abaf39be52191dfaf4fcaa7bb91ece9edcf Mon Sep 17 00:00:00 2001 From: Silarn Date: Sun, 27 Jan 2019 21:51:26 -0600 Subject: Implement an API rate limiter --- src/mainwindow.cpp | 8 ++++++++ src/mainwindow.h | 1 + src/nexusinterface.cpp | 28 ++++++++++++++++++++++++++-- src/nexusinterface.h | 8 ++++++++ src/nxmaccessmanager.cpp | 3 +-- 5 files changed, 44 insertions(+), 4 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9dbb8a86..e37ab150 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -396,6 +396,8 @@ MainWindow::MainWindow(QSettings &initSettings connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString))); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), this, SLOT(updateWindowTitle(const QString&, bool))); + connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), + NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, bool))); connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int))); @@ -430,6 +432,11 @@ MainWindow::MainWindow(QSettings &initSettings connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); m_SaveMetaTimer.start(5000); + m_NexusLimitTimer.setTimerType(Qt::TimerType::PreciseTimer); + m_NexusLimitTimer.setSingleShot(false); + connect(&m_NexusLimitTimer, SIGNAL(timeout()), NexusInterface::instance(&m_PluginContainer), SLOT(calculateRequests())); + m_NexusLimitTimer.start(1000); + setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -5526,6 +5533,7 @@ void MainWindow::nxmDescriptionAvailable(QString gameName, int modID, QVariant u std::vector modsList = ModInfo::getByModID(gameName, modID); for (auto mod : modsList) { mod->setNexusDescription(result["description"].toString()); + mod->setNewestVersion(result["version"].toString()); if ((mod->endorsedState() != ModInfo::ENDORSED_NEVER) && (result.contains("endorsement"))) { QVariantMap endorsement = result["endorsement"].toMap(); diff --git a/src/mainwindow.h b/src/mainwindow.h index 077022db..bf475120 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -348,6 +348,7 @@ private: QTimer m_CheckBSATimer; QTimer m_SaveMetaTimer; QTimer m_UpdateProblemsTimer; + QTimer m_NexusLimitTimer; QFuture m_MetaSave; diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 19fc3af4..760af6a9 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -148,7 +148,7 @@ QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_NMMVersion(), m_PluginContainer(pluginContainer) + : m_NMMVersion(), m_PluginContainer(pluginContainer), m_RemainingRequests(300), m_MaxRequests(300) { m_MOVersion = createVersionInfo(); @@ -190,6 +190,16 @@ void NexusInterface::loginCompleted() nextRequest(); } +void NexusInterface::setRateMax(const QString &userName, bool isPremium) +{ + if (isPremium) { + m_MaxRequests = 600; + m_RemainingRequests = 600; + } else { + m_MaxRequests = 300; + m_RemainingRequests = 300; + } +} void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) { @@ -459,6 +469,11 @@ void NexusInterface::nextRequest() return; } + if (m_RemainingRequests <= 0) { + qWarning() << tr("You've exceeded the Nexus API rate limit and requests are now being throttled."); + return; + } + if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { if (!getAccessManager()->validateAttempted()) { emit needLogin(); @@ -468,6 +483,8 @@ void NexusInterface::nextRequest() } } + m_RemainingRequests--; + NXMRequestInfo info = m_RequestQueue.dequeue(); info.m_Timeout = new QTimer(this); info.m_Timeout->setInterval(60000); @@ -634,6 +651,12 @@ void NexusInterface::requestTimeout() } } +void NexusInterface::calculateRequests() +{ + if (m_RemainingRequests < m_MaxRequests) + m_RemainingRequests++; +} + namespace { QString get_management_url(MOBase::IPluginGame const *game) { @@ -661,7 +684,8 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) , m_Endorse(false) -{} +{ +} NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , QString modVersion diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 56cdef47..022909ce 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -349,6 +349,11 @@ signals: void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); +public slots: + + void setRateMax(const QString &userName, bool isPremium); + void calculateRequests(); + private slots: void requestFinished(); @@ -418,6 +423,9 @@ private: PluginContainer *m_PluginContainer; + int m_RemainingRequests; + int m_MaxRequests; + }; #endif // NEXUSINTERFACE_H diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 5da7b6a3..6847ba25 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -252,8 +252,7 @@ void NXMAccessManager::validateFinished() QString test = jdoc.toJson(); QString name = credentialsData.value("name").toString(); bool premium = credentialsData.value("is_premium?").toBool(); - emit credentialsReceived(credentialsData.value("name").toString(), - credentialsData.value("is_premium?").toBool()); + emit credentialsReceived(name, premium); m_ValidateReply->deleteLater(); m_ValidateReply = nullptr; -- cgit v1.3.1 From 56f9ba159b72a303ea388f466fa4e5ed57a6f45b Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 28 Jan 2019 00:01:55 -0600 Subject: Detect throttling with error code 429 --- src/nexusinterface.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 760af6a9..080b4f7e 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -553,6 +553,11 @@ void NexusInterface::requestFinished(std::list::iterator iter) if (reply->error() != QNetworkReply::NoError) { qWarning("request failed: %s", reply->errorString().toUtf8().constData()); emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString()); + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 429) { + m_RemainingRequests = 0; + qWarning() << tr("Requests have hit the rate limit threshold and are now being throttled."); + } } else { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 301) { -- cgit v1.3.1 From 72de94ee89887c0956a9b76a4d5e97b655b9b26a Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 28 Jan 2019 13:19:26 -0600 Subject: Add a retry for rate limited requests and requeue requests with 429 err --- src/nexusinterface.cpp | 14 +++++++++++--- src/nexusinterface.h | 2 ++ 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 080b4f7e..0b0d990c 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -155,6 +155,10 @@ NexusInterface::NexusInterface(PluginContainer *pluginContainer) m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); m_DiskCache = new QNetworkDiskCache(this); connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); + + m_RetryTimer.setSingleShot(true); + m_RetryTimer.setInterval(1000); + m_RetryTimer.callOnTimeout(this, &NexusInterface::nextRequest); } NXMAccessManager *NexusInterface::getAccessManager() @@ -471,6 +475,7 @@ void NexusInterface::nextRequest() if (m_RemainingRequests <= 0) { qWarning() << tr("You've exceeded the Nexus API rate limit and requests are now being throttled."); + m_RetryTimer.start(); return; } @@ -551,12 +556,15 @@ void NexusInterface::requestFinished(std::list::iterator iter) QNetworkReply *reply = iter->m_Reply; if (reply->error() != QNetworkReply::NoError) { - qWarning("request failed: %s", reply->errorString().toUtf8().constData()); - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString()); int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 429) { m_RemainingRequests = 0; - qWarning() << tr("Requests have hit the rate limit threshold and are now being throttled."); + qWarning("Requests have hit the rate limit threshold and are now being throttled. This request will be retried."); + qWarning("Error: %s", reply->errorString().toUtf8().constData()); + m_RequestQueue.enqueue(*iter); + } else { + qWarning("request failed: %s", reply->errorString().toUtf8().constData()); + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString()); } } else { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 022909ce..e797d34e 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -423,6 +423,8 @@ private: PluginContainer *m_PluginContainer; + QTimer m_RetryTimer; + int m_RemainingRequests; int m_MaxRequests; -- cgit v1.3.1 From 23ff0bea74c05599184d4049a17510b62a49791f Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 28 Jan 2019 13:21:16 -0600 Subject: Always emit the failed signal so we trigger another request --- src/nexusinterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 0b0d990c..25843c51 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -564,8 +564,8 @@ void NexusInterface::requestFinished(std::list::iterator iter) m_RequestQueue.enqueue(*iter); } else { qWarning("request failed: %s", reply->errorString().toUtf8().constData()); - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString()); } + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString()); } else { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 301) { -- cgit v1.3.1 From 4932c52136ce1c8a704c5322e26ca75e55726a15 Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 28 Jan 2019 18:36:19 -0600 Subject: Add counter for API requests --- src/mainwindow.cpp | 26 + src/mainwindow.h | 2 + src/mainwindow.ui | 3437 ++++++++++++++++++++++++------------------------ src/nexusinterface.cpp | 7 +- src/nexusinterface.h | 1 + 5 files changed, 1769 insertions(+), 1704 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f875c95c..2f39bf7a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -363,6 +363,12 @@ MainWindow::MainWindow(QSettings &initSettings ui->bossButton->setToolTip(tr("There is no supported sort mechanism for this game. You will probably have to use a third-party tool.")); } + ui->apiRequests->setAutoFillBackground(true); + QPalette palette = ui->apiRequests->palette(); + palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkGreen); + palette.setColor(ui->apiRequests->foregroundRole(), Qt::white); + ui->apiRequests->setPalette(palette); + connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*))); connect(ui->modList, SIGNAL(dropModeUpdate(bool)), m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool))); @@ -398,6 +404,7 @@ MainWindow::MainWindow(QSettings &initSettings this, SLOT(updateWindowTitle(const QString&, bool))); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, bool))); + connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestsChanged(int, int)), this, SLOT(updateAPICounter(int, int))); connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int))); @@ -5619,6 +5626,25 @@ void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, in } +void MainWindow::updateAPICounter(int queueCount, int requestsRemaining) +{ + ui->apiRequests->setText(QString("API: Q: %1 | T: %2").arg(queueCount).arg(requestsRemaining)); + if (requestsRemaining > 150) { + QPalette palette = ui->apiRequests->palette(); + palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkGreen); + ui->apiRequests->setPalette(palette); + } else if (requestsRemaining < 50) { + QPalette palette = ui->apiRequests->palette(); + palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkRed); + ui->apiRequests->setPalette(palette); + } else { + QPalette palette = ui->apiRequests->palette(); + palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkYellow); + ui->apiRequests->setPalette(palette); + } +} + + BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &progress) { diff --git a/src/mainwindow.h b/src/mainwindow.h index bf475120..900f7cba 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -515,6 +515,8 @@ private slots: void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); + void updateAPICounter(int queueCount, int requestsRemaining); + void editCategories(); void deselectFilters(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 72cfa21b..1aa7ce4c 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -1,1703 +1,1734 @@ - - - MainWindow - - - - 0 - 0 - 926 - 710 - - - - true - - - Mod Organizer - - - - mo_icon.icomo_icon.ico - - - false - - - - - 0 - 0 - - - - - - - Qt::Vertical - - - - - - - - - Categories - - - - 0 - - - 3 - - - 7 - - - 3 - - - 1 - - - - - - 120 - 0 - - - - - 214 - 16777215 - - - - Qt::CustomContextMenu - - - QAbstractItemView::ExtendedSelection - - - 0 - - - true - - - false - - - - 1 - - - - - - - - false - - - - 0 - 0 - - - - - 0 - 25 - - - - Clear - - - true - - - - - - - - 0 - 0 - - - - - - - If checked, only mods that match all selected categories are displayed. - - - And - - - true - - - - - - - If checked, all mods that match at least one of the selected categories are displayed. - - - Or - - - - - - - - - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - 2 - - - - - - - - 0 - 0 - - - - Profile - - - profileBox - - - - - - - Pick a module collection - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different play throughs.</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept seperate for different profiles.</span></p></body></html> - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 16777215 - 16777215 - - - - Open list options... - - - Refresh list. This is usually not necessary unless you modified data outside the program. - - - - - - - :/MO/gui/settings:/MO/gui/settings - - - - 16 - 16 - - - - - - - - Show Open Folders menu... - - - - - - - :/MO/gui/open_folder:/MO/gui/open_folder - - - - - - - Restore Backup... - - - - - - - :/MO/gui/restore:/MO/gui/restore - - - - - - - Create Backup - - - - - - - :/MO/gui/backup:/MO/gui/backup - - - - - - - Active: - - - - - - - - 0 - 26 - - - - This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter. - - - QFrame::Sunken - - - 5 - - - QLCDNumber::Flat - - - - - - - - - - 330 - 400 - - - - - - - - - 64 - 64 - 64 - - - - - - - 255 - 0 - 0 - - - - - - - 0 - 170 - 0 - - - - - - - - - 64 - 64 - 64 - - - - - - - 255 - 0 - 0 - - - - - - - 0 - 170 - 0 - - - - - - - - - 120 - 120 - 120 - - - - - - - 255 - 0 - 0 - - - - - - - 0 - 170 - 0 - - - - - - - - Qt::CustomContextMenu - - - List of available mods. - - - This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders. - - - - - - QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked - - - true - - - true - - - QAbstractItemView::DragDrop - - - Qt::MoveAction - - - true - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - 20 - - - true - - - true - - - true - - - false - - - 35 - - - true - - - false - - - - - - - - - - 20 - 16777215 - - - - x - - - - 20 - 20 - - - - true - - - - - - - - 0 - 0 - - - - Filter - - - - - - - - 8 - true - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - - 95 - 0 - - - - false - - - Qt::RightToLeft - - - border:1px solid #ff0000; - - - Clear all Filters - - - - :/MO/gui/edit_clear:/MO/gui/edit_clear - - - - 12 - 12 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 220 - 0 - - - - Qt::ClickFocus - - - - No groups - - - - - Categories - - - - - Nexus IDs - - - - - - - - - 220 - 0 - - - - Filter - - - - - - - - - - - - - - - - - 0 - 0 - - - - - 0 - 40 - - - - - 9 - 75 - true - - - - Pick a program to run. - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">You can add new Tools to this list, but I can't promise tools I haven't tested will work.</span></p></body></html> - - - - 32 - 32 - - - - false - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 16777215 - 16777215 - - - - - 10 - 75 - true - - - - Run program - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Run the selected program with ModOrganizer enabled.</span></p></body></html> - - - - - - Run - - - - :/MO/gui/run:/MO/gui/run - - - - 36 - 36 - - - - - - - - - 0 - 0 - - - - - 140 - 0 - - - - - 16777215 - 16777215 - - - - - 0 - 0 - - - - Create a shortcut in your start menu or on the desktop to the specified program - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This creates a start menu shortcut that directly starts the selected program with the MO active.</span></p></body></html> - - - Shortcut - - - - :/MO/gui/link:/MO/gui/link - - - - - - - - - - - - - 340 - 250 - - - - - 16777215 - 16777215 - - - - Qt::NoContextMenu - - - QTabWidget::Rounded - - - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Plugins - - - - 6 - - - 6 - - - 6 - - - 0 - - - - - - - true - - - Sort - - - - :/MO/gui/sort:/MO/gui/sort - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Restore Backup... - - - - - - - :/MO/gui/restore:/MO/gui/restore - - - - 16 - 16 - - - - - - - - Create Backup - - - - - - - :/MO/gui/backup:/MO/gui/backup - - - - - - - Active: - - - - - - - - 0 - 26 - - - - This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter. - - - QFrame::Sunken - - - 4 - - - QLCDNumber::Flat - - - - - - - - - - 250 - 250 - - - - - - - - - 64 - 64 - 64 - - - - - - - - - 64 - 64 - 64 - - - - - - - - - 120 - 120 - 120 - - - - - - - - Qt::CustomContextMenu - - - List of available esp/esm files - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.<br />There is a great tool named &quot;BOSS&quot; to automatically sort these files.</span></p></body></html> - - - QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked - - - true - - - false - - - QAbstractItemView::InternalMove - - - Qt::MoveAction - - - true - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - 0 - - - true - - - false - - - true - - - false - - - false - - - - - - - - - - - - Filter - - - - - - - - - - false - - - Archives - - - - 6 - - - 6 - - - 6 - - - 6 - - - - - - - <html><head/><body><p>BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. <p>Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).</p><p>If there is a matching plugin, the game will load them no matter what.</p></body></html> - - - <html><head/><body><p>Currently detected archives. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">What is an archive?</span></a>)</p></body></html> - - - true - - - - - - - - - Qt::CustomContextMenu - - - List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order. - - - BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded. - By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored! - - BSAs checked here are loaded in such a way that your installation order is obeyed properly. - - - false - - - false - - - false - - - 20 - - - true - - - 1 - - - - Archives - - - - - - - - - Data - - - - 6 - - - 6 - - - 6 - - - 6 - - - - - refresh data-directory overview - - - Refresh the overview. This may take a moment. - - - Refresh - - - - :/MO/gui/resources/view-refresh.png:/MO/gui/resources/view-refresh.png - - - - - - - - - Qt::CustomContextMenu - - - This is an overview of your data directory as visible to the game (and tools). - - - true - - - true - - - 400 - - - - File - - - - - Mod - - - - - - - - - - - - Filters the above list so that only conflicts are displayed. - - - Filters the above list so that only conflicts are displayed. - - - Show only conflicts - - - - - - - Filters the above list so that files from archives are not shown - - - - - - Filters the above list so that files from archives are not shown - - - Show files from Archives - - - - - - - - - - Saves - - - - 6 - - - 6 - - - 6 - - - 6 - - - - - Qt::CustomContextMenu - - - - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This is a list of all save games for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.</span></p> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;"></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If you click &quot;Fix Mods...&quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!</span></p></body></html> - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - - - - - - Downloads - - - - 2 - - - 2 - - - 2 - - - 2 - - - - - Refresh downloads view - - - Refresh - - - - :/MO/gui/resources/view-refresh.png:/MO/gui/resources/view-refresh.png - - - - - - - - - - 320 - 0 - - - - Qt::CustomContextMenu - - - true - - - - - - This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here. - - - Qt::ScrollBarAlwaysOn - - - true - - - QAbstractItemView::DragDrop - - - Qt::MoveAction - - - true - - - QAbstractItemView::ScrollPerPixel - - - 0 - - - false - - - true - - - - - - - - - - - Show Hidden - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Filter - - - - - - - - - - - - - - - - - - Qt::ActionsContextMenu - - - QAbstractItemView::NoSelection - - - true - - - false - - - true - - - - - - - - - true - - - Qt::CustomContextMenu - - - Tool Bar - - - false - - - - 42 - 36 - - - - Qt::ToolButtonIconOnly - - - false - - - TopToolBarArea - - - false - - - - - - - - - - - - - - - - - - - :/MO/gui/resources/system-installer.png:/MO/gui/resources/system-installer.png - - - Install Mod - - - Install &Mod - - - Install a new mod from an archive - - - Ctrl+M - - - - - - :/MO/gui/profiles:/MO/gui/profiles - - - Profiles - - - &Profiles - - - Configure Profiles - - - Ctrl+P - - - - - - :/MO/gui/icon_executable:/MO/gui/icon_executable - - - Executables - - - &Executables - - - Configure the executables that can be started through Mod Organizer - - - Ctrl+E - - - - - - :/MO/gui/plugins:/MO/gui/plugins - - - Tools - - - &Tools - - - Tools - - - Ctrl+I - - - - - - :/MO/gui/settings:/MO/gui/settings - - - Settings - - - &Settings - - - Configure settings and workarounds - - - Ctrl+S - - - - - - :/MO/gui/resources/internet-web-browser.png:/MO/gui/resources/internet-web-browser.png - - - Nexus - - - Search nexus network for more mods - - - Ctrl+N - - - - - false - - - - :/MO/gui/update:/MO/gui/update - - - Update - - - Mod Organizer is up-to-date - - - - - false - - - - :/MO/gui/warning:/MO/gui/warning - - - No Notifications - - - This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them. - - - - - - :/MO/gui/help:/MO/gui/help - - - Help - - - Help - - - Ctrl+H - - - - - - :/MO/gui/icon_favorite:/MO/gui/icon_favorite - - - Endorse MO - - - Endorse Mod Organizer - - - - - Copy Log to Clipboard - - - - - - :/MO/gui/instance_switch:/MO/gui/instance_switch - - - Change Game - - - Open the Instance selection dialog to manage a different Game - - - - - - - MOBase::LineEditClear - QLineEdit -
    lineeditclear.h
    -
    - - ModListView - QTreeView -
    modlistview.h
    -
    - - PluginListView - QTreeView -
    pluginlistview.h
    -
    - - LCDNumber - QLCDNumber -
    lcdnumber.h
    -
    - - DownloadListWidget - QTreeView -
    downloadlistwidget.h
    -
    - - MOBase::SortableTreeWidget - QWidget -
    sortabletreewidget.h
    -
    -
    - - - - -
    + + + MainWindow + + + + 0 + 0 + 926 + 710 + + + + true + + + Mod Organizer + + + + mo_icon.icomo_icon.ico + + + false + + + + + 0 + 0 + + + + + + + Qt::Vertical + + + + + + + + + Categories + + + + 0 + + + 3 + + + 7 + + + 3 + + + 1 + + + + + + 120 + 0 + + + + + 214 + 16777215 + + + + Qt::CustomContextMenu + + + QAbstractItemView::ExtendedSelection + + + 0 + + + true + + + false + + + + 1 + + + + + + + + false + + + + 0 + 0 + + + + + 0 + 25 + + + + Clear + + + true + + + + + + + + 0 + 0 + + + + + + + If checked, only mods that match all selected categories are displayed. + + + And + + + true + + + + + + + If checked, all mods that match at least one of the selected categories are displayed. + + + Or + + + + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + 2 + + + + + + + + 0 + 0 + + + + Profile + + + profileBox + + + + + + + Pick a module collection + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different play throughs.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept seperate for different profiles.</span></p></body></html> + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 16777215 + 16777215 + + + + Open list options... + + + Refresh list. This is usually not necessary unless you modified data outside the program. + + + + + + + :/MO/gui/settings:/MO/gui/settings + + + + 16 + 16 + + + + + + + + Show Open Folders menu... + + + + + + + :/MO/gui/open_folder:/MO/gui/open_folder + + + + + + + Restore Backup... + + + + + + + :/MO/gui/restore:/MO/gui/restore + + + + + + + Create Backup + + + + + + + :/MO/gui/backup:/MO/gui/backup + + + + + + + Active: + + + + + + + + 0 + 26 + + + + This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter. + + + QFrame::Sunken + + + 5 + + + QLCDNumber::Flat + + + + + + + + + + 330 + 400 + + + + + + + + + 64 + 64 + 64 + + + + + + + 255 + 0 + 0 + + + + + + + 0 + 170 + 0 + + + + + + + + + 64 + 64 + 64 + + + + + + + 255 + 0 + 0 + + + + + + + 0 + 170 + 0 + + + + + + + + + 120 + 120 + 120 + + + + + + + 255 + 0 + 0 + + + + + + + 0 + 170 + 0 + + + + + + + + Qt::CustomContextMenu + + + List of available mods. + + + This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders. + + + + + + QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + true + + + true + + + QAbstractItemView::DragDrop + + + Qt::MoveAction + + + true + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + 20 + + + true + + + true + + + true + + + false + + + 35 + + + true + + + false + + + + + + + + + + 20 + 16777215 + + + + x + + + + 20 + 20 + + + + true + + + + + + + + 0 + 0 + + + + Filter + + + + + + + + 8 + true + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + API Queued Requests and Throttle Count + + + This tracks the number of queues Nexus API requests on the left and the current requests remaining before being throttled. You have 300 requests as a regular user and 600 as a premium user. These requests renew at the rate of one per second. + + + QFrame::StyledPanel + + + QFrame::Sunken + + + 2 + + + 1 + + + API: Q: 0 | Ts: 300 + + + Qt::AlignCenter + + + 2 + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + + 95 + 0 + + + + false + + + Qt::RightToLeft + + + border:1px solid #ff0000; + + + Clear all Filters + + + + :/MO/gui/edit_clear:/MO/gui/edit_clear + + + + 12 + 12 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 220 + 0 + + + + Qt::ClickFocus + + + + No groups + + + + + Categories + + + + + Nexus IDs + + + + + + + + + 220 + 0 + + + + Filter + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + 9 + 75 + true + + + + Pick a program to run. + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">You can add new Tools to this list, but I can't promise tools I haven't tested will work.</span></p></body></html> + + + + 32 + 32 + + + + false + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 16777215 + 16777215 + + + + + 10 + 75 + true + + + + Run program + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Run the selected program with ModOrganizer enabled.</span></p></body></html> + + + + + + Run + + + + :/MO/gui/run:/MO/gui/run + + + + 36 + 36 + + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + 16777215 + 16777215 + + + + + 0 + 0 + + + + Create a shortcut in your start menu or on the desktop to the specified program + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This creates a start menu shortcut that directly starts the selected program with the MO active.</span></p></body></html> + + + Shortcut + + + + :/MO/gui/link:/MO/gui/link + + + + + + + + + + + + + 340 + 250 + + + + + 16777215 + 16777215 + + + + Qt::NoContextMenu + + + QTabWidget::Rounded + + + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Plugins + + + + 6 + + + 6 + + + 6 + + + 0 + + + + + + + true + + + Sort + + + + :/MO/gui/sort:/MO/gui/sort + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Restore Backup... + + + + + + + :/MO/gui/restore:/MO/gui/restore + + + + 16 + 16 + + + + + + + + Create Backup + + + + + + + :/MO/gui/backup:/MO/gui/backup + + + + + + + Active: + + + + + + + + 0 + 26 + + + + This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter. + + + QFrame::Sunken + + + 4 + + + QLCDNumber::Flat + + + + + + + + + + 250 + 250 + + + + + + + + + 64 + 64 + 64 + + + + + + + + + 64 + 64 + 64 + + + + + + + + + 120 + 120 + 120 + + + + + + + + Qt::CustomContextMenu + + + List of available esp/esm files + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.<br />There is a great tool named &quot;BOSS&quot; to automatically sort these files.</span></p></body></html> + + + QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + true + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + 0 + + + true + + + false + + + true + + + false + + + false + + + + + + + + + + + + Filter + + + + + + + + + + false + + + Archives + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + + <html><head/><body><p>BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. <p>Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).</p><p>If there is a matching plugin, the game will load them no matter what.</p></body></html> + + + <html><head/><body><p>Currently detected archives. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">What is an archive?</span></a>)</p></body></html> + + + true + + + + + + + + + Qt::CustomContextMenu + + + List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order. + + + BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded. + By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored! + + BSAs checked here are loaded in such a way that your installation order is obeyed properly. + + + false + + + false + + + false + + + 20 + + + true + + + 1 + + + + Archives + + + + + + + + + Data + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + refresh data-directory overview + + + Refresh the overview. This may take a moment. + + + Refresh + + + + :/MO/gui/resources/view-refresh.png:/MO/gui/resources/view-refresh.png + + + + + + + + + Qt::CustomContextMenu + + + This is an overview of your data directory as visible to the game (and tools). + + + true + + + true + + + 400 + + + + File + + + + + Mod + + + + + + + + + + + + Filters the above list so that only conflicts are displayed. + + + Filters the above list so that only conflicts are displayed. + + + Show only conflicts + + + + + + + Filters the above list so that files from archives are not shown + + + + + + Filters the above list so that files from archives are not shown + + + Show files from Archives + + + + + + + + + + Saves + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + Qt::CustomContextMenu + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This is a list of all save games for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If you click &quot;Fix Mods...&quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!</span></p></body></html> + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + + + + + Downloads + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Refresh downloads view + + + Refresh + + + + :/MO/gui/resources/view-refresh.png:/MO/gui/resources/view-refresh.png + + + + + + + + + + 320 + 0 + + + + Qt::CustomContextMenu + + + true + + + + + + This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here. + + + Qt::ScrollBarAlwaysOn + + + true + + + QAbstractItemView::DragDrop + + + Qt::MoveAction + + + true + + + QAbstractItemView::ScrollPerPixel + + + 0 + + + false + + + true + + + + + + + + + + + Show Hidden + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filter + + + + + + + + + + + + + + + + + + Qt::ActionsContextMenu + + + QAbstractItemView::NoSelection + + + true + + + false + + + true + + + + + + + + + true + + + Qt::CustomContextMenu + + + Tool Bar + + + false + + + + 42 + 36 + + + + Qt::ToolButtonIconOnly + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + + + + + + + + :/MO/gui/resources/system-installer.png:/MO/gui/resources/system-installer.png + + + Install Mod + + + Install &Mod + + + Install a new mod from an archive + + + Ctrl+M + + + + + + :/MO/gui/profiles:/MO/gui/profiles + + + Profiles + + + &Profiles + + + Configure Profiles + + + Ctrl+P + + + + + + :/MO/gui/icon_executable:/MO/gui/icon_executable + + + Executables + + + &Executables + + + Configure the executables that can be started through Mod Organizer + + + Ctrl+E + + + + + + :/MO/gui/plugins:/MO/gui/plugins + + + Tools + + + &Tools + + + Tools + + + Ctrl+I + + + + + + :/MO/gui/settings:/MO/gui/settings + + + Settings + + + &Settings + + + Configure settings and workarounds + + + Ctrl+S + + + + + + :/MO/gui/resources/internet-web-browser.png:/MO/gui/resources/internet-web-browser.png + + + Nexus + + + Search nexus network for more mods + + + Ctrl+N + + + + + false + + + + :/MO/gui/update:/MO/gui/update + + + Update + + + Mod Organizer is up-to-date + + + + + false + + + + :/MO/gui/warning:/MO/gui/warning + + + No Notifications + + + This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them. + + + + + + :/MO/gui/help:/MO/gui/help + + + Help + + + Help + + + Ctrl+H + + + + + + :/MO/gui/icon_favorite:/MO/gui/icon_favorite + + + Endorse MO + + + Endorse Mod Organizer + + + + + Copy Log to Clipboard + + + + + + :/MO/gui/instance_switch:/MO/gui/instance_switch + + + Change Game + + + Open the Instance selection dialog to manage a different Game + + + + + + + MOBase::LineEditClear + QLineEdit +
    lineeditclear.h
    +
    + + ModListView + QTreeView +
    modlistview.h
    +
    + + PluginListView + QTreeView +
    pluginlistview.h
    +
    + + LCDNumber + QLCDNumber +
    lcdnumber.h
    +
    + + DownloadListWidget + QTreeView +
    downloadlistwidget.h
    +
    + + MOBase::SortableTreeWidget + QWidget +
    sortabletreewidget.h
    +
    +
    + + + + +
    diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 25843c51..c149f987 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -203,6 +203,7 @@ void NexusInterface::setRateMax(const QString &userName, bool isPremium) m_MaxRequests = 300; m_RemainingRequests = 300; } + emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); } void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) @@ -543,6 +544,7 @@ void NexusInterface::nextRequest() connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); info.m_Timeout->start(); m_ActiveRequest.push_back(info); + emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); } @@ -559,6 +561,7 @@ void NexusInterface::requestFinished(std::list::iterator iter) int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 429) { m_RemainingRequests = 0; + emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); qWarning("Requests have hit the rate limit threshold and are now being throttled. This request will be retried."); qWarning("Error: %s", reply->errorString().toUtf8().constData()); m_RequestQueue.enqueue(*iter); @@ -666,8 +669,10 @@ void NexusInterface::requestTimeout() void NexusInterface::calculateRequests() { - if (m_RemainingRequests < m_MaxRequests) + if (m_RemainingRequests < m_MaxRequests) { m_RemainingRequests++; + emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); + } } namespace { diff --git a/src/nexusinterface.h b/src/nexusinterface.h index e797d34e..a68948e6 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -348,6 +348,7 @@ signals: void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); + void requestsChanged(int queueCount, int requestsRemaining); public slots: -- cgit v1.3.1 From a305ea2270c02172973f365986f194652ad344ec Mon Sep 17 00:00:00 2001 From: Silarn Date: Tue, 29 Jan 2019 01:35:13 -0600 Subject: Translate nexus game name to short name to fix fetching updates --- src/mainwindow.cpp | 26 ++++++++++++++++++++------ src/nexusinterface.cpp | 5 +++-- 2 files changed, 23 insertions(+), 8 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2f39bf7a..02720d44 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5455,14 +5455,14 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD bool foundUpdate = false; m_ModsToUpdate--; bool sameNexus = false; + QString gameNameReal; for (IPluginGame *game : m_PluginContainer.plugins()) { - if (game->gameShortName() == gameName) { - if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID()) - sameNexus = true; + if (game->gameNexusName() == gameName) { + gameNameReal = game->gameShortName(); break; } } - std::vector modsList = ModInfo::getByModID(gameName, modID); + std::vector modsList = ModInfo::getByModID(gameNameReal, modID); // Not clear to me what this is accomplishing? //if (sameNexus) { // std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), modID); @@ -5553,7 +5553,14 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD void MainWindow::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { QVariantMap result = resultData.toMap(); - std::vector modsList = ModInfo::getByModID(gameName, modID); + QString gameNameReal; + for (IPluginGame *game : m_PluginContainer.plugins()) { + if (game->gameNexusName() == gameName) { + gameNameReal = game->gameShortName(); + break; + } + } + std::vector modsList = ModInfo::getByModID(gameNameReal, modID); for (auto mod : modsList) { mod->setNexusDescription(result["description"].toString()); mod->setNewestVersion(result["version"].toString()); @@ -5615,7 +5622,14 @@ void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, in statusBar()->hide(); } if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) { - std::vector modsList = ModInfo::getByModID(gameName, modID); + QString gameNameReal; + for (IPluginGame *game : m_PluginContainer.plugins()) { + if (game->gameNexusName() == gameName) { + gameNameReal = game->gameShortName(); + break; + } + } + std::vector modsList = ModInfo::getByModID(gameNameReal, modID); for (auto mod : modsList) { mod->setNexusID(-1); } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index c149f987..1e50f0b2 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -157,7 +157,7 @@ NexusInterface::NexusInterface(PluginContainer *pluginContainer) connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); m_RetryTimer.setSingleShot(true); - m_RetryTimer.setInterval(1000); + m_RetryTimer.setInterval(2000); m_RetryTimer.callOnTimeout(this, &NexusInterface::nextRequest); } @@ -476,7 +476,8 @@ void NexusInterface::nextRequest() if (m_RemainingRequests <= 0) { qWarning() << tr("You've exceeded the Nexus API rate limit and requests are now being throttled."); - m_RetryTimer.start(); + if (!m_RetryTimer.isActive()) + m_RetryTimer.start(); return; } -- cgit v1.3.1 From ef286a938d79f74947a392d049068e4818ccf7fd Mon Sep 17 00:00:00 2001 From: Silarn Date: Tue, 29 Jan 2019 15:12:59 -0600 Subject: Multiple fixes: * Uses current daily/hourly rate limits * Fixed issue with translating API game string to mod game string ** Corrects issues with update checks and downloads --- src/downloadmanager.cpp | 29 +++++- src/downloadmanager.h | 3 + src/mainwindow.cpp | 20 ++-- src/mainwindow.h | 3 +- src/nexusinterface.cpp | 56 +++++------ src/nexusinterface.h | 11 ++- src/nxmaccessmanager.cpp | 10 +- src/nxmaccessmanager.h | 240 +++++++++++++++++++++++------------------------ 8 files changed, 201 insertions(+), 171 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 2ba46d74..f5e4d688 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -19,11 +19,11 @@ along with Mod Organizer. If not, see . #include "downloadmanager.h" +#include "organizercore.h" #include "nxmurl.h" #include "nexusinterface.h" #include "nxmaccessmanager.h" #include "iplugingame.h" -#include "downloadmanager.h" #include #include #include "utility.h" @@ -203,6 +203,7 @@ QString DownloadManager::DownloadInfo::currentURL() DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false) { + m_OrganizerCore = dynamic_cast(parent); connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); m_TimeoutTimer.setSingleShot(false); //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout())); @@ -1532,7 +1533,16 @@ void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int file info->description = BBCode::convertToHTML(result["changelog_html"].toString()); info->repository = "Nexus"; - info->gameName = gameName; + + QStringList games(m_ManagedGame->validShortNames()); + games += m_ManagedGame->gameShortName(); + for (auto game : games) { + MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game); + if (gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { + info->gameName = gamePlugin->gameShortName(); + } + } + info->modID = modID; info->fileID = fileID; @@ -1597,10 +1607,21 @@ void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int m_RequestIDs.erase(idIter); } + QString gameShortName; + QStringList games(m_ManagedGame->validShortNames()); + games += m_ManagedGame->gameShortName(); + for (auto game : games) { + MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game); + if (gamePlugin->gameNexusName() == gameName) { + gameShortName = gamePlugin->gameShortName(); + break; + } + } + ModRepositoryFileInfo *info = qobject_cast(qvariant_cast(userData)); QVariantList resultList = resultData.toList(); if (resultList.length() == 0) { - removePending(gameName, modID, fileID); + removePending(gameShortName, modID, fileID); emit showMessage(tr("No download server available. Please try again later.")); return; } @@ -1614,7 +1635,7 @@ void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int foreach (const QVariant &server, resultList) { URLs.append(server.toMap()["URI"].toString()); } - addDownload(URLs, gameName, modID, fileID, info); + addDownload(URLs, gameShortName, modID, fileID, info); } diff --git a/src/downloadmanager.h b/src/downloadmanager.h index 3c36143e..841a9fbc 100644 --- a/src/downloadmanager.h +++ b/src/downloadmanager.h @@ -40,6 +40,7 @@ namespace MOBase { class IPluginGame; } class NexusInterface; class PluginContainer; +class OrganizerCore; /*! * \brief manages downloading of files and provides progress information for gui elements @@ -532,6 +533,8 @@ private: NexusInterface *m_NexusInterface; + OrganizerCore *m_OrganizerCore; + QVector> m_PendingDownloads; QVector m_ActiveDownloads; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 02720d44..094a44af 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -402,9 +402,9 @@ MainWindow::MainWindow(QSettings &initSettings connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString))); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), this, SLOT(updateWindowTitle(const QString&, bool))); - connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), - NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, bool))); - connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestsChanged(int, int)), this, SLOT(updateAPICounter(int, int))); + connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool, std::tuple)), + NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, bool, std::tuple))); + connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestsChanged(int, std::tuple)), this, SLOT(updateAPICounter(int, std::tuple))); connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int))); @@ -439,11 +439,6 @@ MainWindow::MainWindow(QSettings &initSettings connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); m_SaveMetaTimer.start(5000); - m_NexusLimitTimer.setTimerType(Qt::TimerType::PreciseTimer); - m_NexusLimitTimer.setSingleShot(false); - connect(&m_NexusLimitTimer, SIGNAL(timeout()), NexusInterface::instance(&m_PluginContainer), SLOT(calculateRequests())); - m_NexusLimitTimer.start(1000); - setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -5640,14 +5635,15 @@ void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, in } -void MainWindow::updateAPICounter(int queueCount, int requestsRemaining) +void MainWindow::updateAPICounter(int queueCount, std::tuple limits) { - ui->apiRequests->setText(QString("API: Q: %1 | T: %2").arg(queueCount).arg(requestsRemaining)); - if (requestsRemaining > 150) { + ui->apiRequests->setText(QString("API: Q: %1 | D: %2 | H: %3").arg(queueCount).arg(std::get<0>(limits)).arg(std::get<2>(limits))); + int requestsRemaining = std::get<0>(limits) + std::get<2>(limits); + if (requestsRemaining > 300) { QPalette palette = ui->apiRequests->palette(); palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkGreen); ui->apiRequests->setPalette(palette); - } else if (requestsRemaining < 50) { + } else if (requestsRemaining < 150) { QPalette palette = ui->apiRequests->palette(); palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkRed); ui->apiRequests->setPalette(palette); diff --git a/src/mainwindow.h b/src/mainwindow.h index 900f7cba..b3490d07 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -348,7 +348,6 @@ private: QTimer m_CheckBSATimer; QTimer m_SaveMetaTimer; QTimer m_UpdateProblemsTimer; - QTimer m_NexusLimitTimer; QFuture m_MetaSave; @@ -515,7 +514,7 @@ private slots: void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); - void updateAPICounter(int queueCount, int requestsRemaining); + void updateAPICounter(int queueCount, std::tuple limits); void editCategories(); void deselectFilters(); diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 1e50f0b2..7481cf96 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -148,7 +148,7 @@ QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_NMMVersion(), m_PluginContainer(pluginContainer), m_RemainingRequests(300), m_MaxRequests(300) + : m_NMMVersion(), m_PluginContainer(pluginContainer), m_RemainingDailyRequests(0), m_RemainingHourlyRequests(0), m_MaxDailyRequests(0), m_MaxHourlyRequests(0) { m_MOVersion = createVersionInfo(); @@ -194,16 +194,13 @@ void NexusInterface::loginCompleted() nextRequest(); } -void NexusInterface::setRateMax(const QString &userName, bool isPremium) +void NexusInterface::setRateMax(const QString&, bool, std::tuple limits) { - if (isPremium) { - m_MaxRequests = 600; - m_RemainingRequests = 600; - } else { - m_MaxRequests = 300; - m_RemainingRequests = 300; - } - emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); + m_RemainingDailyRequests = std::get<0>(limits); + m_MaxDailyRequests = std::get<1>(limits); + m_RemainingHourlyRequests = std::get<2>(limits); + m_MaxHourlyRequests = std::get<3>(limits); + emit requestsChanged(m_RequestQueue.size(), limits); } void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) @@ -474,10 +471,16 @@ void NexusInterface::nextRequest() return; } - if (m_RemainingRequests <= 0) { - qWarning() << tr("You've exceeded the Nexus API rate limit and requests are now being throttled."); - if (!m_RetryTimer.isActive()) - m_RetryTimer.start(); + if (m_RemainingDailyRequests + m_RemainingHourlyRequests <= 0) { + if (!m_RetryTimer.isActive()) { + QTime time = QTime::currentTime(); + QTime targetTime; + targetTime.setHMS((time.hour() + 1) % 23, 0, 5); + m_RetryTimer.start(time.msecsTo(targetTime)); + QString warning("You've exceeded the Nexus API rate limit and requests are now being throttled. " + "Your next batch of requests will be available in approximately %1 minutes and %2 seconds."); + qWarning() << warning.arg(time.secsTo(targetTime) / 60).arg(time.secsTo(targetTime) % 60); + } return; } @@ -490,8 +493,6 @@ void NexusInterface::nextRequest() } } - m_RemainingRequests--; - NXMRequestInfo info = m_RequestQueue.dequeue(); info.m_Timeout = new QTimer(this); info.m_Timeout->setInterval(60000); @@ -545,7 +546,6 @@ void NexusInterface::nextRequest() connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); info.m_Timeout->start(); m_ActiveRequest.push_back(info); - emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); } @@ -561,8 +561,6 @@ void NexusInterface::requestFinished(std::list::iterator iter) if (reply->error() != QNetworkReply::NoError) { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 429) { - m_RemainingRequests = 0; - emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); qWarning("Requests have hit the rate limit threshold and are now being throttled. This request will be retried."); qWarning("Error: %s", reply->errorString().toUtf8().constData()); m_RequestQueue.enqueue(*iter); @@ -617,6 +615,18 @@ void NexusInterface::requestFinished(std::list::iterator iter) } } } + + m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); + m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt(); + m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt(); + m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt(); + + emit requestsChanged(m_RequestQueue.size(), std::tuple(std::make_tuple( + m_RemainingDailyRequests, + m_MaxDailyRequests, + m_RemainingHourlyRequests, + m_MaxHourlyRequests + ))); } @@ -668,14 +678,6 @@ void NexusInterface::requestTimeout() } } -void NexusInterface::calculateRequests() -{ - if (m_RemainingRequests < m_MaxRequests) { - m_RemainingRequests++; - emit requestsChanged(m_RequestQueue.size(), m_RemainingRequests); - } -} - namespace { QString get_management_url(MOBase::IPluginGame const *game) { diff --git a/src/nexusinterface.h b/src/nexusinterface.h index a68948e6..e60ebf3f 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -348,12 +348,11 @@ signals: void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); - void requestsChanged(int queueCount, int requestsRemaining); + void requestsChanged(int queueCount, std::tuple requestsRemaining); public slots: - void setRateMax(const QString &userName, bool isPremium); - void calculateRequests(); + void setRateMax(const QString&, bool, std::tuple limits); private slots: @@ -426,8 +425,10 @@ private: QTimer m_RetryTimer; - int m_RemainingRequests; - int m_MaxRequests; + int m_RemainingDailyRequests; + int m_RemainingHourlyRequests; + int m_MaxDailyRequests; + int m_MaxHourlyRequests; }; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 6847ba25..d9bb2905 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -252,7 +252,15 @@ void NXMAccessManager::validateFinished() QString test = jdoc.toJson(); QString name = credentialsData.value("name").toString(); bool premium = credentialsData.value("is_premium?").toBool(); - emit credentialsReceived(name, premium); + + std::tuple limits(std::make_tuple( + m_ValidateReply->rawHeader("x-rl-daily-remaining").toInt(), + m_ValidateReply->rawHeader("x-rl-daily-limit").toInt(), + m_ValidateReply->rawHeader("x-rl-hourly-remaining").toInt(), + m_ValidateReply->rawHeader("x-rl-hourly-limit").toInt() + )); + + emit credentialsReceived(name, premium, limits); m_ValidateReply->deleteLater(); m_ValidateReply = nullptr; diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index b316ef77..7fdb508f 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -1,120 +1,120 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef NXMACCESSMANAGER_H -#define NXMACCESSMANAGER_H - - -#include -#include -#include -#include -#include - -namespace MOBase { class IPluginGame; } - -/** - * @brief access manager extended to handle nxm links - **/ -class NXMAccessManager : public QNetworkAccessManager -{ - Q_OBJECT -public: - - explicit NXMAccessManager(QObject *parent, const QString &moVersion); - - ~NXMAccessManager(); - - void setNMMVersion(const QString &nmmVersion); - - bool validated() const; - - bool validateAttempted() const; - bool validateWaiting() const; - - void apiCheck(const QString &apiKey); - - void showCookies() const; - - void clearCookies(); - - QString userAgent(const QString &subModule = QString()) const; - - QString apiKey() const; - - void startValidationCheck(); - - void refuseValidation(); - -signals: - - /** - * @brief emitted when a nxm:// link is opened - * - * @param url the nxm-link - **/ - void requestNXMDownload(const QString &url); - - /** - * @brief emitted after a successful login or if login was not necessary - * - * @param necessary true if a login was necessary and succeeded, false if the user is still logged in - **/ - void validateSuccessful(bool necessary); - - void validateFailed(const QString &message); - - void credentialsReceived(const QString &userName, bool premium); - -private slots: - - void validateFinished(); - void validateError(QNetworkReply::NetworkError errorCode); - void validateTimeout(); - -protected: - - virtual QNetworkReply *createRequest( - QNetworkAccessManager::Operation operation, const QNetworkRequest &request, - QIODevice *device); - -private: - - QTimer m_ValidateTimeout; - QNetworkReply *m_ValidateReply; - QProgressDialog *m_ProgressDialog { nullptr }; - - QString m_MOVersion; - QString m_NMMVersion; - - QString m_ApiKey; - - bool m_ValidateAttempted; - enum { - VALIDATE_NOT_CHECKED, - VALIDATE_CHECKING, - VALIDATE_NOT_VALID, - VALIDATE_ATTEMPT_FAILED, - VALIDATE_REFUSED, - VALIDATE_VALID - } m_ValidateState = VALIDATE_NOT_CHECKED; - -}; - -#endif // NXMACCESSMANAGER_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef NXMACCESSMANAGER_H +#define NXMACCESSMANAGER_H + + +#include +#include +#include +#include +#include + +namespace MOBase { class IPluginGame; } + +/** + * @brief access manager extended to handle nxm links + **/ +class NXMAccessManager : public QNetworkAccessManager +{ + Q_OBJECT +public: + + explicit NXMAccessManager(QObject *parent, const QString &moVersion); + + ~NXMAccessManager(); + + void setNMMVersion(const QString &nmmVersion); + + bool validated() const; + + bool validateAttempted() const; + bool validateWaiting() const; + + void apiCheck(const QString &apiKey); + + void showCookies() const; + + void clearCookies(); + + QString userAgent(const QString &subModule = QString()) const; + + QString apiKey() const; + + void startValidationCheck(); + + void refuseValidation(); + +signals: + + /** + * @brief emitted when a nxm:// link is opened + * + * @param url the nxm-link + **/ + void requestNXMDownload(const QString &url); + + /** + * @brief emitted after a successful login or if login was not necessary + * + * @param necessary true if a login was necessary and succeeded, false if the user is still logged in + **/ + void validateSuccessful(bool necessary); + + void validateFailed(const QString &message); + + void credentialsReceived(const QString &userName, bool premium, std::tuple limits); + +private slots: + + void validateFinished(); + void validateError(QNetworkReply::NetworkError errorCode); + void validateTimeout(); + +protected: + + virtual QNetworkReply *createRequest( + QNetworkAccessManager::Operation operation, const QNetworkRequest &request, + QIODevice *device); + +private: + + QTimer m_ValidateTimeout; + QNetworkReply *m_ValidateReply; + QProgressDialog *m_ProgressDialog { nullptr }; + + QString m_MOVersion; + QString m_NMMVersion; + + QString m_ApiKey; + + bool m_ValidateAttempted; + enum { + VALIDATE_NOT_CHECKED, + VALIDATE_CHECKING, + VALIDATE_NOT_VALID, + VALIDATE_ATTEMPT_FAILED, + VALIDATE_REFUSED, + VALIDATE_VALID + } m_ValidateState = VALIDATE_NOT_CHECKED; + +}; + +#endif // NXMACCESSMANAGER_H -- cgit v1.3.1 From 6b9e94473addf468224ea2b0521ab724d5842cb5 Mon Sep 17 00:00:00 2001 From: Silarn Date: Thu, 31 Jan 2019 00:36:41 -0600 Subject: Various fixes and updates * Add second api type for mod info to segment updates from basic desc * Add saved nexus file type and factor it into the update display * Fix some issues with how we were checking for 'latest update' files --- src/downloadmanager.cpp | 3 ++- src/mainwindow.cpp | 36 ++++++++++++++++++++---------------- src/mainwindow.h | 2 +- src/modinfo.h | 12 ++++++++++++ src/modinfobackup.h | 5 +++++ src/modinfodialog.cpp | 1 + src/modinfoforeign.h | 6 ++++-- src/modinfooverwrite.h | 6 ++++-- src/modinforegular.cpp | 20 ++++++++++++++++++-- src/modinforegular.h | 14 ++++++++++++++ src/modinfoseparator.h | 6 ++++-- src/modlist.cpp | 5 +++++ src/nexusinterface.cpp | 32 +++++++++++++++++++++++++------- src/nexusinterface.h | 43 ++++++++++++++++++++++++++++++++++++++++--- 14 files changed, 155 insertions(+), 36 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index f5e4d688..32d05230 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -1004,7 +1004,8 @@ QString DownloadManager::getFileTypeString(int fileType) case 2: return tr("Update"); case 3: return tr("Optional"); case 4: return tr("Old"); - case 5: return tr("Misc"); + case 5: return tr("Miscellaneous"); + case 6: return tr("Deleted"); default: return tr("Unknown"); } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6933efda..064f817d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -441,7 +441,7 @@ MainWindow::MainWindow(QSettings &initSettings m_ModUpdateTimer.setSingleShot(false); connect(&m_ModUpdateTimer, SIGNAL(timeout()), this, SLOT(modUpdateCheck())); - m_ModUpdateTimer.start(300 * 1000); + //m_ModUpdateTimer.start(300 * 1000); setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -496,7 +496,7 @@ MainWindow::MainWindow(QSettings &initSettings updatePluginCount(); updateModCount(); - modUpdateCheck(); + //modUpdateCheck(); } @@ -5472,9 +5472,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD QVariantMap resultInfo = resultData.toMap(); QList files = resultInfo["files"].toList(); QList fileUpdates = resultInfo["file_updates"].toList(); - bool foundUpdate = false; m_ModsToUpdate--; - bool sameNexus = false; QString gameNameReal; for (IPluginGame *game : m_PluginContainer.plugins()) { if (game->gameNexusName() == gameName) { @@ -5485,7 +5483,19 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD std::vector modsList = ModInfo::getByModID(gameNameReal, modID); for (auto mod : modsList) { + bool foundUpdate = false; + bool oldFile = false; QString installedFile = mod->getInstallationFile(); + QVariantMap foundFile; + for (auto file : files) { + QVariantMap fileData = file.toMap(); + if (fileData["file_name"].toString().compare(installedFile, Qt::CaseInsensitive) == 0) { + foundFile = fileData; + mod->setNexusFileStatus(foundFile["category_id"].toInt()); + if (mod->getNexusFileStatus() == 4 || mod->getNexusFileStatus() == 6) + oldFile = true; + } + } for (auto update : fileUpdates) { QVariantMap updateData = update.toMap(); // Locate the current install file as an update @@ -5514,17 +5524,11 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } break; - } else if (installedFile == updateData["new_file_name"]) { + } else if (installedFile == updateData["new_file_name"].toString()) { // This is a safety mechanism if this is the latest update file so we don't use the mod version - if (!foundUpdate) { - for (auto file : files) { - QVariantMap fileData = file.toMap(); - if (fileData["file_id"].toInt() == updateData["new_file_id"]) { - mod->setVersion(fileData["version"].toString()); - mod->setNewestVersion(fileData["version"].toString()); - foundUpdate = true; - } - } + if (!foundUpdate && !oldFile) { + foundUpdate = true; + mod->setNewestVersion(foundFile["version"].toString()); } } } @@ -5535,7 +5539,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD mod->updateNXMInfo(); } else { // Scrape mod data here so we can use the mod version if no file update was located - NexusInterface::instance(&m_PluginContainer)->requestDescription(gameName, modID, this, QVariant(), QString()); + NexusInterface::instance(&m_PluginContainer)->requestModInfo(gameName, modID, this, QVariant(), QString()); } } @@ -5546,7 +5550,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } } -void MainWindow::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { QVariantMap result = resultData.toMap(); QString gameNameReal; diff --git a/src/mainwindow.h b/src/mainwindow.h index 65ca99dd..3f3a5457 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -512,7 +512,7 @@ private slots: void modUpdateCheck(); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); diff --git a/src/modinfo.h b/src/modinfo.h index 9c753752..ae9dd1f3 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -503,6 +503,18 @@ public: */ virtual QString getDescription() const = 0; + /** + * @return the nexus file status (aka category ID) + */ + virtual int getNexusFileStatus() const = 0; + + + /** + * @brief sets the file status (category ID) from Nexus + * @param status the status id of the installed file + */ + virtual void setNexusFileStatus(int status) = 0; + /** * @return comments for this mod */ diff --git a/src/modinfobackup.h b/src/modinfobackup.h index f74cd111..e88335e2 100644 --- a/src/modinfobackup.h +++ b/src/modinfobackup.h @@ -25,7 +25,12 @@ public: virtual std::vector getIniTweaks() const { return std::vector(); } virtual std::vector getFlags() const; virtual QString getDescription() const; + virtual int getNexusFileStatus() const { return 0; } + virtual void setNexusFileStatus(int) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void setLastNexusQuery(QDateTime) {} + virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } + virtual void setLastNexusUpdate(QDateTime) {} virtual void getNexusFiles(QList::const_iterator&, QList::const_iterator&) {} virtual QString getNexusDescription() const { return QString(); } diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index db3e5de1..30d09579 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -851,6 +851,7 @@ QString ModInfoDialog::getFileCategory(int categoryID) case 2: return tr("Update"); case 3: return tr("Optional"); case 4: return tr("Old"); + case 5: return tr("Miscellaneous"); case 6: return tr("Deleted"); default: return tr("Unknown"); } diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 0702f268..6fcf56a1 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -47,10 +47,12 @@ public: virtual std::vector getFlags() const; virtual int getHighlight() const; virtual QString getDescription() const; + virtual int getNexusFileStatus() const { return 0; } + virtual void setNexusFileStatus(int) {} virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } - virtual void setLastNexusUpdate(QDateTime time) {} + virtual void setLastNexusUpdate(QDateTime) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } - virtual void setLastNexusQuery(QDateTime time) {} + virtual void setLastNexusQuery(QDateTime) {} virtual QString getNexusDescription() const { return QString(); } virtual int getFixedPriority() const { return INT_MIN; } virtual QStringList archives(bool checkOnDisk = false) { return m_Archives; } diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index d2882301..9f77f648 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -50,10 +50,12 @@ public: virtual std::vector getFlags() const; virtual int getHighlight() const; virtual QString getDescription() const; + virtual int getNexusFileStatus() const { return 0; } + virtual void setNexusFileStatus(int) {} virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } - virtual void setLastNexusUpdate(QDateTime time) {} + virtual void setLastNexusUpdate(QDateTime) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } - virtual void setLastNexusQuery(QDateTime time) {} + virtual void setLastNexusQuery(QDateTime) {} virtual QString getNexusDescription() const { return QString(); } virtual QStringList archives(bool checkOnDisk = false); virtual void addInstalledFile(int, int) {} diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index f2844084..89541a70 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -93,6 +93,7 @@ void ModInfoRegular::readMeta() m_IgnoredVersion = metaFile.value("ignoredVersion", "").toString(); m_InstallationFile = metaFile.value("installationFile", "").toString(); m_NexusDescription = metaFile.value("nexusDescription", "").toString(); + m_NexusFileStatus = metaFile.value("nexusFileStatus", "1").toInt(); m_Repository = metaFile.value("repository", "Nexus").toString(); m_Converted = metaFile.value("converted", false).toBool(); m_Validated = metaFile.value("validated", false).toBool(); @@ -161,6 +162,7 @@ void ModInfoRegular::saveMeta() metaFile.setValue("notes", m_Notes); metaFile.setValue("nexusDescription", m_NexusDescription); metaFile.setValue("url", m_URL); + metaFile.setValue("nexusFileStatus", m_NexusFileStatus); metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); metaFile.setValue("lastNexusUpdate", m_LastNexusUpdate.toString(Qt::ISODate)); metaFile.setValue("converted", m_Converted); @@ -199,6 +201,9 @@ bool ModInfoRegular::updateAvailable() const if (m_IgnoredVersion.isValid() && (m_IgnoredVersion == m_NewestVersion)) { return false; } + if (m_NexusFileStatus == 4 || m_NexusFileStatus == 6) { + return true; + } return m_NewestVersion.isValid() && (m_Version < m_NewestVersion); } @@ -271,8 +276,6 @@ bool ModInfoRegular::updateNXMInfo() m_NexusBridge.requestDescription(m_GameName, m_NexusID, QVariant()); return true; } - QString warning("Please wait until %1 to request updated mod info from Nexus!"); - qWarning() << warning.arg(target.toLocalTime().time().toString(Qt::DefaultLocaleShortDate)); return false; } @@ -613,6 +616,19 @@ QString ModInfoRegular::getDescription() const } } +int ModInfoRegular::getNexusFileStatus() const +{ + return m_NexusFileStatus; +} + +void ModInfoRegular::setNexusFileStatus(int status) +{ + m_NexusFileStatus = status; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + QString ModInfoRegular::comments() const { return m_Comments; diff --git a/src/modinforegular.h b/src/modinforegular.h index 8e3952e8..13ce7e42 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -285,6 +285,19 @@ public: */ virtual QString getDescription() const; + + /** + * @return the nexus file status (aka category ID) + */ + virtual int getNexusFileStatus() const; + + + /** + * @brief sets the file status (category ID) from Nexus + * @param status the status id of the installed file + */ + virtual void setNexusFileStatus(int status); + /** * @return comments for this mod */ @@ -402,6 +415,7 @@ private: bool m_IsAlternate; bool m_Converted; bool m_Validated; + bool m_NexusFileStatus; MOBase::VersionInfo m_NewestVersion; MOBase::VersionInfo m_IgnoredVersion; diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index 1f3be861..819ee686 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -43,10 +43,12 @@ public: virtual QString getInstallationFile() const { return ""; } virtual QString getURL() const { return ""; } virtual QString repository() const { return ""; } + virtual int getNexusFileStatus() const { return 0; } + virtual void setNexusFileStatus(int) {} virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } - virtual void setLastNexusUpdate(QDateTime time) {} + virtual void setLastNexusUpdate(QDateTime) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } - virtual void setLastNexusQuery(QDateTime time) {} + virtual void setLastNexusQuery(QDateTime) {} virtual QDateTime creationTime() const { return QDateTime(); } virtual void getNexusFiles diff --git a/src/modlist.cpp b/src/modlist.cpp index 8c502f99..3f8ea4e1 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -458,6 +458,11 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. " "Either way you may want to \"upgrade\"."); } + if (modInfo->getNexusFileStatus() == 4) { + text += "
    " + tr("This file has been marked as \"Old\". There is most likely an updated version of this file available."); + } else if (modInfo->getNexusFileStatus() == 6) { + text += "
    " + tr("This file has been marked as \"Deleted\"! You may want to check for an update or remove the nexus ID from this mod!"); + } if (modInfo->getNexusID() > 0) { if (!modInfo->canBeUpdated()) { text += "
    " + tr("This mod was last checked on %1. It will be available to check after %2.") diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 7481cf96..06fbe451 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -326,6 +326,22 @@ int NexusInterface::requestDescription(QString gameName, int modID, QObject *rec return requestInfo.m_ID; } +int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule) @@ -504,12 +520,11 @@ void NexusInterface::nextRequest() if (!info.m_Reroute) { bool hasParams = false; switch (info.m_Type) { - case NXMRequestInfo::TYPE_DESCRIPTION: { + case NXMRequestInfo::TYPE_DESCRIPTION: + case NXMRequestInfo::TYPE_MODINFO: { url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); } break; - case NXMRequestInfo::TYPE_FILES: { - url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); - } break; + case NXMRequestInfo::TYPE_FILES: case NXMRequestInfo::TYPE_GETUPDATES: { url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); } break; @@ -594,18 +609,21 @@ void NexusInterface::requestFinished(std::list::iterator iter) case NXMRequestInfo::TYPE_DESCRIPTION: { emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_MODINFO: { + emit nxmModInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; case NXMRequestInfo::TYPE_FILES: { emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_GETUPDATES: { + emit nxmUpdatesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; case NXMRequestInfo::TYPE_FILEINFO: { emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); } break; case NXMRequestInfo::TYPE_DOWNLOADURL: { emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); } break; - case NXMRequestInfo::TYPE_GETUPDATES: { - emit nxmUpdatesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; diff --git a/src/nexusinterface.h b/src/nexusinterface.h index e60ebf3f..64f15961 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -151,6 +151,7 @@ public: /** * @brief request description for a mod * + * @param gameName the game short name to support multiple game sources * @param modID id of the mod caller is interested in (assumed to be for the current game) * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) * @param userData user data to be returned with the result @@ -164,6 +165,7 @@ public: /** * @brief request description for a mod * + * @param gameName the game short name to support multiple game sources * @param modID id of the mod caller is interested in * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) * @param userData user data to be returned with the result @@ -173,12 +175,39 @@ public: int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + /** + * @brief request description for a mod + * + * @param gameName the game short name to support multiple game sources + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param receiver the object to receive the result asynchronously via a signal (nxmModInfoAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestModInfo(gameName, modID, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @brief request mod info + * + * @param gameName the game short name to support multiple game sources + * @param modID id of the mod caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmModInfoAvailable) + * @param userData user data to be returned with the result + * @param game Game with which the mod is associated + * @return int an id to identify the request + **/ + int requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + /** * @brief request nexus descriptions for multiple mods at once - * @param modIDs a list of ids of mods the caller is interested in + * @param modID id of the mod the caller is interested in * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) * @param userData user data to be returned with the result - * @param game the game with which the mods are associated + * @param gameName the game with which the mods are associated * @return int an id to identify the request */ int requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule); @@ -186,6 +215,7 @@ public: /** * @brief request a list of the files belonging to a mod * + * @param gameName the game short name to support multiple game sources * @param modID id of the mod caller is interested in (assumed to be for the current game) * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) * @param userData user data to be returned with the result @@ -200,6 +230,7 @@ public: /** * @brief request a list of the files belonging to a mod * + * @param gameName the game short name to support multiple game sources * @param modID id of the mod caller is interested in * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) * @param userData user data to be returned with the result @@ -212,7 +243,7 @@ public: /** * @brief request info about a single file of a mod * - * @param game name of the game short name to request the download from + * @param gameName name of the game short name to request the download from * @param modID id of the mod caller is interested in (assumed to be for the current game) * @param fileID id of the file the caller is interested in * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) @@ -224,6 +255,7 @@ public: /** * @brief request the download url of a file * + * @param gameName the game short name to support multiple game sources * @param modID id of the mod caller is interested in (assumed to be for the current game) * @param fileID id of the file the caller is interested in * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) @@ -238,6 +270,7 @@ public: /** * @brief request the download url of a file * + * @param gameName the game short name to support multiple game sources * @param modID id of the mod caller is interested in * @param fileID id of the file the caller is interested in * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) @@ -248,6 +281,7 @@ public: int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); /** + * @param gameName the game short name to support multiple game sources * @brief toggle endorsement state of the mod * @param modID id of the mod (assumed to be for the current game) * @param endorse true if the mod should be endorsed, false for un-endorse @@ -261,6 +295,7 @@ public: } /** + * @param gameName the game short name to support multiple game sources * @brief toggle endorsement state of the mod * @param modID id of the mod * @param endorse true if the mod should be endorsed, false for un-endorse @@ -342,6 +377,7 @@ signals: void needLogin(); void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); @@ -374,6 +410,7 @@ private: QNetworkReply *m_Reply; enum Type { TYPE_DESCRIPTION, + TYPE_MODINFO, TYPE_FILES, TYPE_FILEINFO, TYPE_DOWNLOADURL, -- cgit v1.3.1 From b7d2a608d8c0364d76950bc908c53203e3f467ff Mon Sep 17 00:00:00 2001 From: Silarn Date: Thu, 31 Jan 2019 16:54:31 -0600 Subject: Fixe API Key storage and actually validate the response --- src/nexusinterface.cpp | 2 +- src/nxmaccessmanager.cpp | 39 +++++++++++++++++++++------------------ src/settings.cpp | 8 ++------ 3 files changed, 24 insertions(+), 25 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 06fbe451..3ea7fdac 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -148,7 +148,7 @@ QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_NMMVersion(), m_PluginContainer(pluginContainer), m_RemainingDailyRequests(0), m_RemainingHourlyRequests(0), m_MaxDailyRequests(0), m_MaxHourlyRequests(0) + : m_NMMVersion(), m_PluginContainer(pluginContainer), m_RemainingDailyRequests(2500), m_RemainingHourlyRequests(100), m_MaxDailyRequests(2500), m_MaxHourlyRequests(100) { m_MOVersion = createVersionInfo(); diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index d9bb2905..77ffbebc 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -249,23 +249,26 @@ void NXMAccessManager::validateFinished() if (m_ValidateReply != nullptr) { QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll()); QJsonObject credentialsData = jdoc.object(); - QString test = jdoc.toJson(); - QString name = credentialsData.value("name").toString(); - bool premium = credentialsData.value("is_premium?").toBool(); - - std::tuple limits(std::make_tuple( - m_ValidateReply->rawHeader("x-rl-daily-remaining").toInt(), - m_ValidateReply->rawHeader("x-rl-daily-limit").toInt(), - m_ValidateReply->rawHeader("x-rl-hourly-remaining").toInt(), - m_ValidateReply->rawHeader("x-rl-hourly-limit").toInt() - )); - - emit credentialsReceived(name, premium, limits); - - m_ValidateReply->deleteLater(); - m_ValidateReply = nullptr; - - m_ValidateState = VALIDATE_VALID; - emit validateSuccessful(true); + if (credentialsData.contains("user_id")) { + QString name = credentialsData.value("name").toString(); + bool premium = credentialsData.value("is_premium").toBool(); + + std::tuple limits(std::make_tuple( + m_ValidateReply->rawHeader("x-rl-daily-remaining").toInt(), + m_ValidateReply->rawHeader("x-rl-daily-limit").toInt(), + m_ValidateReply->rawHeader("x-rl-hourly-remaining").toInt(), + m_ValidateReply->rawHeader("x-rl-hourly-limit").toInt() + )); + + emit credentialsReceived(name, premium, limits); + + m_ValidateReply->deleteLater(); + m_ValidateReply = nullptr; + + m_ValidateState = VALIDATE_VALID; + emit validateSuccessful(true); + } else { + emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString())); + } } } diff --git a/src/settings.cpp b/src/settings.cpp index eb34ab29..42f2e43f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -181,9 +181,7 @@ QString Settings::obfuscate(const QString &info) QString result; DATA_BLOB input; DATA_BLOB output; - std::vector data; - data.reserve(info.size() + 1); - memcpy(data.data(), info.constData(), info.size() + 1); + std::vector data(info.toUtf8().begin(), info.toUtf8().end()); DWORD cbInput = info.size() + 1; input.pbData = data.data(); input.cbData = cbInput; @@ -204,9 +202,7 @@ QString Settings::deObfuscate(const QString &info) QByteArray realInfo = QByteArray::fromBase64(info.toUtf8()); DATA_BLOB input; DATA_BLOB output; - std::vector data; - data.reserve(realInfo.size() + 1); - memcpy(data.data(), realInfo.constData(), realInfo.size() + 1); + std::vector data(realInfo.begin(), realInfo.end()); DWORD cbInput = realInfo.size() + 1; input.pbData = data.data(); input.cbData = cbInput; -- cgit v1.3.1 From d08b836265c3f2c44041cb4d6da37183245b9a0e Mon Sep 17 00:00:00 2001 From: Silarn Date: Thu, 31 Jan 2019 17:16:17 -0600 Subject: Fix various issues when not having a stored API key --- src/mainwindow.cpp | 14 +++++--------- src/nexusinterface.cpp | 24 ++++++++++++------------ src/nxmaccessmanager.cpp | 2 ++ src/settings.cpp | 7 ++++--- 4 files changed, 23 insertions(+), 24 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9368192f..3a341298 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3996,10 +3996,8 @@ void MainWindow::checkModsForUpdates() if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { // otherwise there will be no endorsement info - MessageDialog::showMessage(tr("Not logged in, endorsement information will be wrong"), - this, true); - m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); + } else { + qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); } } @@ -5457,12 +5455,10 @@ void MainWindow::modUpdateCheck() } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([this]() { this->checkModsForUpdates(); }); + m_OrganizerCore.doAfterLogin([this]() { this->modUpdateCheck(); }); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { // otherwise there will be no endorsement info - MessageDialog::showMessage(tr("Not logged in, endorsement information will be wrong"), - this, true); - m_ModsToUpdate += ModInfo::autoUpdateCheck(&m_PluginContainer, this); + } else { + qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); } } } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 3ea7fdac..16f96154 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -628,23 +628,23 @@ void NexusInterface::requestFinished(std::list::iterator iter) emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; } + + m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); + m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt(); + m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt(); + m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt(); + + emit requestsChanged(m_RequestQueue.size(), std::tuple(std::make_tuple( + m_RemainingDailyRequests, + m_MaxDailyRequests, + m_RemainingHourlyRequests, + m_MaxHourlyRequests + ))); } else { emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), tr("invalid response")); } } } - - m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); - m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt(); - m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt(); - m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt(); - - emit requestsChanged(m_RequestQueue.size(), std::tuple(std::make_tuple( - m_RemainingDailyRequests, - m_MaxDailyRequests, - m_RemainingHourlyRequests, - m_MaxHourlyRequests - ))); } diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 77ffbebc..d1720c76 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -270,5 +270,7 @@ void NXMAccessManager::validateFinished() } else { emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString())); } + } else { + emit validateFailed(tr("unknown error")); } } diff --git a/src/settings.cpp b/src/settings.cpp index 42f2e43f..cd0aec8e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -178,11 +178,12 @@ void Settings::registerPlugin(IPlugin *plugin) QString Settings::obfuscate(const QString &info) { + QByteArray byteData = info.toUtf8(); QString result; DATA_BLOB input; DATA_BLOB output; - std::vector data(info.toUtf8().begin(), info.toUtf8().end()); - DWORD cbInput = info.size() + 1; + std::vector data(byteData.begin(), byteData.end()); + DWORD cbInput = data.size() + 1; input.pbData = data.data(); input.cbData = cbInput; @@ -203,7 +204,7 @@ QString Settings::deObfuscate(const QString &info) DATA_BLOB input; DATA_BLOB output; std::vector data(realInfo.begin(), realInfo.end()); - DWORD cbInput = realInfo.size() + 1; + DWORD cbInput = data.size() + 1; input.pbData = data.data(); input.cbData = cbInput; -- cgit v1.3.1 From 7f56407b916f6ca204e72f25c01965441e0228b1 Mon Sep 17 00:00:00 2001 From: Silarn Date: Thu, 31 Jan 2019 23:19:16 -0600 Subject: Use NXM link key and expiration in file list API call when available --- src/downloadmanager.cpp | 10 ++++++++-- src/nexusinterface.cpp | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 32d05230..1db8b0eb 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -575,7 +575,13 @@ void DownloadManager::addNXMDownload(const QString &url) emit update(-1); emit downloadAdded(); - m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId(), "")); + ModRepositoryFileInfo *info = new ModRepositoryFileInfo(); + + info->nexusKey = nxmInfo.key(); + info->nexusExpires = nxmInfo.expires(); + + QObject *test = info; + m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, qVariantFromValue(test), "")); } @@ -1520,7 +1526,7 @@ void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int file m_RequestIDs.erase(idIter); } - ModRepositoryFileInfo *info = new ModRepositoryFileInfo(); + ModRepositoryFileInfo *info = qobject_cast(qvariant_cast(userData)); QVariantMap result = resultData.toMap(); info->name = result["name"].toString(); diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 16f96154..1d6d2015 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -532,7 +532,12 @@ void NexusInterface::nextRequest() url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); } break; case NXMRequestInfo::TYPE_DOWNLOADURL: { - url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); + ModRepositoryFileInfo *fileInfo = qobject_cast(qvariant_cast(info.m_UserData)); + if (!fileInfo->nexusKey.isEmpty() && fileInfo->nexusExpires) + url = QString("%1/games/%2/mods/%3/files/%4/download_link?key=%5&expires=%6") + .arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID).arg(fileInfo->nexusKey).arg(fileInfo->nexusExpires); + else + url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); } break; case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { QString endorse = info.m_Endorse ? "endorse" : "abstain"; -- cgit v1.3.1 From 2b5862ef0144d49e1927b5c914cbcea7183fab8a Mon Sep 17 00:00:00 2001 From: Silarn Date: Fri, 1 Feb 2019 17:45:05 -0600 Subject: Multiple updates: * Remove periodic auto-update * Add context menu force-update for mod list * Fix some bugs with update parsing - Ignore update files that are deleted - Fix remaining issue with failing to get all mods due to capitalization * Remove queue retry when out of requests * Clear all requests when out of requests * More potential improvements to login/validation process --- src/mainwindow.cpp | 54 +++++++++++++++++++++++++++++------------------- src/mainwindow.h | 4 ++-- src/modinfo.cpp | 42 ++++++++++++++++++------------------- src/modinfo.h | 2 +- src/nexusinterface.cpp | 30 ++++++++++----------------- src/nexusinterface.h | 2 -- src/nxmaccessmanager.cpp | 4 ++++ 7 files changed, 71 insertions(+), 67 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3a341298..fecf8704 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -439,10 +439,6 @@ MainWindow::MainWindow(QSettings &initSettings connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); m_SaveMetaTimer.start(5000); - m_ModUpdateTimer.setSingleShot(false); - connect(&m_ModUpdateTimer, SIGNAL(timeout()), this, SLOT(modUpdateCheck())); - m_ModUpdateTimer.start(300 * 1000); - setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -495,8 +491,6 @@ MainWindow::MainWindow(QSettings &initSettings updatePluginCount(); updateModCount(); - - modUpdateCheck(); } @@ -4056,6 +4050,22 @@ void MainWindow::ignoreUpdate() { } } +void MainWindow::checkModUpdates_clicked() +{ + std::multimap IDs; + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + IDs.insert(std::make_pair(info->getGameName(), info->getNexusID())); + } + } else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + IDs.insert(std::make_pair(info->getGameName(), info->getNexusID())); + } + modUpdateCheck(IDs); +} + void MainWindow::unignoreUpdate() { QItemSelectionModel *selection = ui->modList->selectionModel(); @@ -4411,7 +4421,7 @@ void MainWindow::initModListContextMenu(QMenu *menu) menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - menu->addAction(tr("Force update check"), this, SLOT(checkModsForUpdates())); + menu->addAction(tr("Check for updates"), this, SLOT(checkModsForUpdates())); menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh())); menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); } @@ -4518,10 +4528,11 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) menu.addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme())); } + if (info->getNexusID() > 0) + menu->addAction(tr("Force-check updates"), this, SLOT(checkModUpdates_clicked())); if (info->updateIgnored()) { menu.addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate())); - } - else { + } else { if (info->updateAvailable() || info->downgradeAvailable()) { menu.addAction(tr("Ignore update"), this, SLOT(ignoreUpdate())); } @@ -5447,15 +5458,15 @@ void MainWindow::modDetailsUpdated(bool) } } -void MainWindow::modUpdateCheck() +void MainWindow::modUpdateCheck(std::multimap IDs) { if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - m_ModsToUpdate += ModInfo::autoUpdateCheck(&m_PluginContainer, this); + m_ModsToUpdate += ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs); m_RefreshProgress->setRange(0, m_ModsToUpdate); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([this]() { this->modUpdateCheck(); }); + m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); }); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); } else { qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); @@ -5506,19 +5517,20 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD if (currentUpdate == updateScanData["old_file_id"].toInt()) { currentUpdate = updateScanData["new_file_id"].toInt(); finalUpdate = false; + // Apply the version data from the latest file + for (auto file : files) { + QVariantMap fileData = file.toMap(); + if (fileData["file_id"].toInt() == currentUpdate) { + if (fileData["category_id"].toInt() != 6) { + mod->setNewestVersion(fileData["version"].toString()); + foundUpdate = true; + } + } + } break; } } } - // Apply the version data from the latest file - for (auto file : files) { - QVariantMap fileData = file.toMap(); - if (fileData["file_id"].toInt() == currentUpdate) { - mod->setNewestVersion(fileData["version"].toString()); - foundUpdate = true; - } - } - break; } else if (installedFile == updateData["new_file_name"].toString()) { // This is a safety mechanism if this is the latest update file so we don't use the mod version diff --git a/src/mainwindow.h b/src/mainwindow.h index 3f3a5457..59be9800 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -348,7 +348,6 @@ private: QTimer m_CheckBSATimer; QTimer m_SaveMetaTimer; QTimer m_UpdateProblemsTimer; - QTimer m_ModUpdateTimer; QFuture m_MetaSave; @@ -509,7 +508,7 @@ private slots: void modInstalled(const QString &modName); - void modUpdateCheck(); + void modUpdateCheck(std::multimap IDs); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); @@ -590,6 +589,7 @@ private slots: void overwriteClosed(int); void changeVersioningScheme(); + void checkModUpdates_clicked(); void ignoreUpdate(); void unignoreUpdate(); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index e9cfd77b..a5b240c8 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -148,8 +148,7 @@ std::vector ModInfo::getByModID(QString game, int modID) for (auto iter : s_ModsByModID) { if (iter.first.second == modID) { if (iter.first.first.compare(game, Qt::CaseInsensitive) == 0) { - match = iter.second; - break; + match.insert(match.end(), iter.second.begin(), iter.second.end()); } } } @@ -303,6 +302,9 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } result = organizedGames.size(); + + if (organizedGames.empty()) + qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); for (auto game : organizedGames) { NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); @@ -312,32 +314,28 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } -int ModInfo::autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver) +int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) { - qInfo("Initializing periodic update check."); - int result = 0; - - std::vector> sortedMods; + std::vector> mods; std::multimap organizedGames; - for (auto mod : s_Collection) { - if (mod->canBeUpdated()) { - sortedMods.push_back(mod); - } + + for (auto ID : IDs) { + auto matchedMods = getByModID(ID.first, ID.second); + mods.insert(mods.end(), matchedMods.begin(), matchedMods.end()); } + mods.erase( + std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return !mod->canBeUpdated(); }), + mods.end() + ); - std::sort(sortedMods.begin(), sortedMods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { + std::sort(mods.begin(), mods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { return a->getLastNexusUpdate() < b->getLastNexusUpdate(); }); - if (sortedMods.size() > 10) - sortedMods.resize(10); - - result = sortedMods.size(); + if (mods.size()) { + qInfo("Checking updates for %d mods...", mods.size()); - if (sortedMods.size()) { - qInfo("Checking updates for %d mods...", sortedMods.size()); - - for (auto mod : sortedMods) { + for (auto mod : mods) { organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); } @@ -345,10 +343,10 @@ int ModInfo::autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); } } else { - qInfo("No mods require updates at this time."); + qInfo("None of the selected mods can be updated."); } - return result; + return organizedGames.size(); } diff --git a/src/modinfo.h b/src/modinfo.h index ae9dd1f3..9343a98c 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -188,7 +188,7 @@ public: /** * @brief run a limited batch of mod update checks for "newest version" information */ - static int autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver); + static int manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs); /** * @brief query nexus information for every mod and update the "newest version" information diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 1d6d2015..130a87b1 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -155,10 +155,6 @@ NexusInterface::NexusInterface(PluginContainer *pluginContainer) m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); m_DiskCache = new QNetworkDiskCache(this); connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); - - m_RetryTimer.setSingleShot(true); - m_RetryTimer.setInterval(2000); - m_RetryTimer.callOnTimeout(this, &NexusInterface::nextRequest); } NXMAccessManager *NexusInterface::getAccessManager() @@ -468,8 +464,6 @@ IPluginGame* NexusInterface::getGame(QString gameName) const void NexusInterface::cleanup() { -// delete m_AccessManager; -// delete m_DiskCache; m_AccessManager = nullptr; m_DiskCache = nullptr; } @@ -487,19 +481,6 @@ void NexusInterface::nextRequest() return; } - if (m_RemainingDailyRequests + m_RemainingHourlyRequests <= 0) { - if (!m_RetryTimer.isActive()) { - QTime time = QTime::currentTime(); - QTime targetTime; - targetTime.setHMS((time.hour() + 1) % 23, 0, 5); - m_RetryTimer.start(time.msecsTo(targetTime)); - QString warning("You've exceeded the Nexus API rate limit and requests are now being throttled. " - "Your next batch of requests will be available in approximately %1 minutes and %2 seconds."); - qWarning() << warning.arg(time.secsTo(targetTime) / 60).arg(time.secsTo(targetTime) % 60); - } - return; - } - if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { if (!getAccessManager()->validateAttempted()) { emit needLogin(); @@ -509,6 +490,17 @@ void NexusInterface::nextRequest() } } + if (m_RemainingDailyRequests + m_RemainingHourlyRequests <= 0) { + m_RequestQueue.clear(); + QTime time = QTime::currentTime(); + QTime targetTime; + targetTime.setHMS((time.hour() + 1) % 23, 5, 0); + QString warning("You've exceeded the Nexus API rate limit and requests are now being throttled. " + "Your next batch of requests will be available in approximately %1 minutes and %2 seconds."); + qWarning() << warning.arg(time.secsTo(targetTime) / 60).arg(time.secsTo(targetTime) % 60); + return; + } + NXMRequestInfo info = m_RequestQueue.dequeue(); info.m_Timeout = new QTimer(this); info.m_Timeout->setInterval(60000); diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 64f15961..db4ebfd6 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -460,8 +460,6 @@ private: PluginContainer *m_PluginContainer; - QTimer m_RetryTimer; - int m_RemainingDailyRequests; int m_RemainingHourlyRequests; int m_MaxDailyRequests; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index d1720c76..7ab474a7 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -268,9 +268,13 @@ void NXMAccessManager::validateFinished() m_ValidateState = VALIDATE_VALID; emit validateSuccessful(true); } else { + m_ApiKey.clear(); + m_ValidateState = VALIDATE_NOT_VALID; emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString())); } } else { + m_ApiKey.clear(); + m_ValidateState = VALIDATE_NOT_CHECKED; emit validateFailed(tr("unknown error")); } } -- cgit v1.3.1 From f7220cdd2207ae67901c01684a951eb5fc1135fa Mon Sep 17 00:00:00 2001 From: Silarn Date: Fri, 1 Feb 2019 19:31:22 -0600 Subject: Prevent runaway requests and fix info dialog description update display --- src/modinfodialog.cpp | 118 +++++++++++-------------------------------------- src/nexusinterface.cpp | 3 +- 2 files changed, 26 insertions(+), 95 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index 30d09579..d733f851 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -798,52 +798,6 @@ void ModInfoDialog::refreshNexusData(int modID) } -/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - QVariantMap result = resultData.toMap(); - - if (!result["description"].isNull()) { - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(result["description"].toString())); - -// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - ui->descriptionView->setHtml(descriptionAsHTML); - } else { - ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - } - - QLineEdit *versionEdit = findChild("versionEdit"); - QString version = result["version"].toString(); - - if (!version.isEmpty()) { - m_ModInfo->setNewestVersion(version); - - VersionInfo currentVersion(versionEdit->text()); - VersionInfo newestVersion(version); - - QPalette versionColor; - if (currentVersion < newestVersion) { - versionColor.setColor(QPalette::Text, Qt::red); - versionEdit->setToolTip(tr("Current Version: %1").arg(version)); - } else { - versionColor.setColor(QPalette::Text, Qt::green); - versionEdit->setToolTip(tr("No update available")); - } - versionEdit->setPalette(versionColor); - } -}*/ - - QString ModInfoDialog::getFileCategory(int categoryID) { switch (categoryID) { @@ -876,46 +830,21 @@ void ModInfoDialog::updateVersionColor() void ModInfoDialog::modDetailsUpdated(bool success) { - if (success) { - QString nexusDescription = m_ModInfo->getNexusDescription(); - if (!nexusDescription.isEmpty()) { - /* QString input = - "[size=20]sizetest[/size]\r\n" - "[COLOR=yellow]colortest[/COLOR]\r\n" - "[center]centertest[/center]\r\n" - "[quote]quotetest 1[/quote]\r\n" - "[quote=bla]quotetest 2[/quote]\r\n" - "[url]www.skyrimnexus.com[/url]\r\n" - "[url=www.skyrimnexus.com]urltest 2[/url]\r\n" - "[ol]\r\n" - "[li]item 2[/li]" - "[*]item 1\r\n" - "[/ol]\r\n" - "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n" - "[table][tr][th]headertest1[/th]" - "[th]headertest2[/th][/tr]" - "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]" - "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]" - "[email=\"sherb@gmx.net\"]mail me[/email]"; - ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/ - - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(nexusDescription)); - - ui->descriptionView->page()->setHtml(descriptionAsHTML); - - // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - // ui->descriptionView->setHtml(descriptionAsHTML); - } else { - // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - ui->descriptionView->page()->setHtml(tr("(description incomplete, please visit nexus)")); - } + QString nexusDescription = m_ModInfo->getNexusDescription(); + QString descriptionAsHTML = "" + "" + "%1" + ""; - updateVersionColor(); + if (!nexusDescription.isEmpty()) { + descriptionAsHTML = descriptionAsHTML.arg(BBCode::convertToHTML(nexusDescription)); + } else { + descriptionAsHTML = descriptionAsHTML.arg(tr("

    Uh oh!

    Sorry, there is no description available for this mod. :(

    ")); } + + ui->descriptionView->page()->setHtml(descriptionAsHTML); + + updateVersionColor(); } @@ -923,21 +852,21 @@ void ModInfoDialog::activateNexusTab() { QLineEdit *modIDEdit = findChild("modIDEdit"); int modID = modIDEdit->text().toInt(); - if (modID != 0) { + if (modID > 0) { QString nexusLink = NexusInterface::instance(m_PluginContainer)->getModURL(modID, m_ModInfo->getGameName()); QLabel *visitNexusLabel = findChild("visitNexusLabel"); visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink)); visitNexusLabel->setToolTip(nexusLink); - if (m_ModInfo->getNexusDescription().isEmpty() || - QDateTime::currentDateTimeUtc() > m_ModInfo->getLastNexusQuery().addDays(1)) { + if (m_ModInfo->getNexusDescription().isEmpty() || QDateTime::currentDateTimeUtc() >= m_ModInfo->getLastNexusQuery().addDays(1)) { refreshNexusData(modID); } else { - this->modDetailsUpdated(true); + modDetailsUpdated(true); } - } + } else + modDetailsUpdated(true); QLineEdit *versionEdit = findChild("versionEdit"); - QString currentVersion = m_Settings->value("version", "0.0").toString(); + QString currentVersion = m_Settings->value("version", "???").toString(); versionEdit->setText(currentVersion); ui->customUrlLineEdit->setText(m_ModInfo->getURL()); } @@ -1521,9 +1450,12 @@ void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, void ModInfoDialog::on_refreshButton_clicked() { - m_ModInfo->updateNXMInfo(); - - MessageDialog::showMessage(tr("Info requested, please wait"), this); + if (m_ModInfo->getNexusID() > 0) { + QLineEdit *modIDEdit = findChild("modIDEdit"); + int modID = modIDEdit->text().toInt(); + refreshNexusData(modID); + } else + qInfo("Mod has no valid Nexus ID, info can't be updated."); } void ModInfoDialog::on_endorseBtn_clicked() diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 130a87b1..fee8a427 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -573,9 +573,8 @@ void NexusInterface::requestFinished(std::list::iterator iter) if (reply->error() != QNetworkReply::NoError) { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 429) { - qWarning("Requests have hit the rate limit threshold and are now being throttled. This request will be retried."); + qWarning("All API requests have been consumed and are now being denied."); qWarning("Error: %s", reply->errorString().toUtf8().constData()); - m_RequestQueue.enqueue(*iter); } else { qWarning("request failed: %s", reply->errorString().toUtf8().constData()); } -- cgit v1.3.1 From 34a374fce8b96c064f2c3e020e3f86167365eef5 Mon Sep 17 00:00:00 2001 From: Silarn Date: Sat, 2 Feb 2019 13:18:36 -0600 Subject: Several api improvements: * Don't check time for force updates * Implement API restrictions for <200 remaining requests * Fix bug causing duplicate mod checks for differing short name capitalization --- src/modinfo.cpp | 6 ++--- src/nexusinterface.cpp | 67 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 44 insertions(+), 29 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index a5b240c8..555dcb68 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -297,7 +297,7 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv std::multimap organizedGames; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { - organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); } } @@ -324,7 +324,7 @@ int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiv mods.insert(mods.end(), matchedMods.begin(), matchedMods.end()); } mods.erase( - std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return !mod->canBeUpdated(); }), + std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return mod->getNexusID() <= 0; }), mods.end() ); @@ -336,7 +336,7 @@ int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiv qInfo("Checking updates for %d mods...", mods.size()); for (auto mod : mods) { - organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); } for (auto game : organizedGames) { diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index fee8a427..55201c2b 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -325,35 +325,45 @@ int NexusInterface::requestDescription(QString gameName, int modID, QObject *rec int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; } int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule) { - IPluginGame *game = getGame(gameName); - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + IPluginGame *game = getGame(gameName); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; } @@ -429,18 +439,23 @@ int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); - requestInfo.m_Endorse = endorse; - m_RequestQueue.enqueue(requestInfo); + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); + requestInfo.m_Endorse = endorse; + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; } bool NexusInterface::requiresLogin(const NXMRequestInfo &info) -- cgit v1.3.1 From 91e909bc45e8b33b0a774c71ed28175243f0cccf Mon Sep 17 00:00:00 2001 From: Silarn Date: Sat, 2 Feb 2019 13:23:31 -0600 Subject: Request pools are not combined, we need to use the max of both pools. --- src/nexusinterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 55201c2b..bc45d8f6 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -505,7 +505,7 @@ void NexusInterface::nextRequest() } } - if (m_RemainingDailyRequests + m_RemainingHourlyRequests <= 0) { + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) <= 0) { m_RequestQueue.clear(); QTime time = QTime::currentTime(); QTime targetTime; -- cgit v1.3.1 From a873c7be23f270ab1a929aaf72aa03182e0ddc2c Mon Sep 17 00:00:00 2001 From: Silarn Date: Tue, 5 Feb 2019 13:53:07 -0600 Subject: Simple updates API headers requested by Nexus --- src/nexusinterface.cpp | 5 +++-- src/nxmaccessmanager.cpp | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index bc45d8f6..f1fa252a 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -557,10 +557,11 @@ void NexusInterface::nextRequest() url = info.m_URL; } QNetworkRequest request(url); - request.setRawHeader("apikey", m_AccessManager->apiKey().toUtf8()); + request.setRawHeader("APIKEY", m_AccessManager->apiKey().toUtf8()); request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule)); request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - request.setRawHeader("Protocol-Version", "0.5.5"); + request.setRawHeader("Protocol-Version", "1.0.0"); + request.setRawHeader("Application-Name", "MO2"); request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8()); if (postData.object().isEmpty()) diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 7ab474a7..1e0ac49b 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -122,7 +122,11 @@ void NXMAccessManager::startValidationCheck() QNetworkRequest request(requestString); request.setRawHeader("APIKEY", m_ApiKey.toUtf8()); - request.setRawHeader("User-Agent", userAgent().toUtf8()); + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, userAgent().toUtf8()); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Protocol-Version", "1.0.0"); + request.setRawHeader("Application-Name", "MO2"); + request.setRawHeader("Application-Version", m_MOVersion.toUtf8()); m_ProgressDialog = new QProgressDialog(nullptr); m_ProgressDialog->setLabelText(tr("Validating Nexus Connection")); @@ -192,12 +196,19 @@ void NXMAccessManager::apiCheck(const QString &apiKey) QString NXMAccessManager::userAgent(const QString &subModule) const { QStringList comments; + QString os; + if (QSysInfo::productType() == "windows") + comments << ((QSysInfo::kernelType() == "winnt") ? "Windows_NT " : "Windows ") + QSysInfo::kernelVersion(); + else + comments << QSysInfo::kernelType().left(1).toUpper() + QSysInfo::kernelType().mid(1) + << QSysInfo::productType().left(1).toUpper() + QSysInfo::kernelType().mid(1) + " " + QSysInfo::productVersion(); comments << "Nexus Client v" + m_NMMVersion; if (!subModule.isEmpty()) { comments << "module: " + subModule; } + comments << ((QSysInfo::buildCpuArchitecture() == "x86_64") ? "x64" : "x86"); - return QString("Mod Organizer/%1 (%2)").arg(m_MOVersion, comments.join("; ")); + return QString("Mod Organizer/%1 (%2) Qt/%3").arg(m_MOVersion, comments.join("; "), qVersion()); } -- cgit v1.3.1 From 5a7d0243c69d5194ee1a4d0be05d2bcaec18aeac Mon Sep 17 00:00:00 2001 From: Silarn Date: Sat, 9 Feb 2019 04:52:44 -0600 Subject: Implement bulk update check layer --- src/mainwindow.cpp | 33 +++++++++++++++++++++-- src/mainwindow.h | 1 + src/modinfo.cpp | 73 ++++++++++++++++++++++++++++++++++++++++++++------ src/modinfo.h | 2 ++ src/modinforegular.cpp | 4 +-- src/nexusinterface.cpp | 64 +++++++++++++++++++++++++++++++++++++++++++ src/nexusinterface.h | 24 ++++++++++++++++- 7 files changed, 188 insertions(+), 13 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a17b51d5..5e97a959 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5468,10 +5468,39 @@ void MainWindow::modUpdateCheck(std::multimap IDs) if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); }); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { + } else qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); + } +} + +void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int) +{ + QString gameNameReal; + for (IPluginGame *game : m_PluginContainer.plugins()) { + if (game->gameNexusName() == gameName) { + gameNameReal = game->gameShortName(); + break; + } + } + QVariantList resultList = resultData.toList(); + std::set> finalMods = ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true); + + if (finalMods.empty()) { + qInfo("None of your mods appear to have had recent file updates."); + } + + std::set> organizedGames; + for (auto mod : finalMods) { + if (mod->canBeUpdated()) { + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); } } + + if (!finalMods.empty() && organizedGames.empty()) + qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); + + for (auto game : organizedGames) + NexusInterface::instance(&m_PluginContainer)->requestUpdates(game.second, this, QVariant(), game.first, QString()); } void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -5594,7 +5623,7 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD mod->setIsEndorsed(false); } mod->setLastNexusQuery(QDateTime::currentDateTimeUtc()); - mod->setNexusLastModified(QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt())); + mod->setNexusLastModified(QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC)); mod->saveMeta(); } } diff --git a/src/mainwindow.h b/src/mainwindow.h index 59be9800..d2704b72 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -510,6 +510,7 @@ private slots: void modUpdateCheck(std::multimap IDs); + void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index a1059e24..3e694baf 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -294,25 +294,82 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); //} - std::set> organizedGames; + QDateTime earliest = QDateTime::currentDateTimeUtc(); + QDateTime latest; + std::set games; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { - organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); + if (mod->getLastNexusUpdate() < earliest) + earliest = mod->getLastNexusUpdate(); + if (mod->getLastNexusUpdate() > latest) + latest = mod->getLastNexusUpdate(); + games.insert(mod->getGameName().toLower()); } } - result = organizedGames.size(); - - if (organizedGames.empty()) - qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); + if (latest < QDateTime::currentDateTimeUtc().addDays(-30)) { + std::set> organizedGames; + for (auto mod : s_Collection) { + if (mod->canBeUpdated()) { + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); + } + } + + result = organizedGames.size(); + + if (organizedGames.empty()) + qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); - for (auto game : organizedGames) { - NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + for (auto game : organizedGames) { + NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + } + } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-30)) { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString()); + } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-7)) { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(false), QString()); + } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-1)) { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::WEEK, receiver, QVariant(false), QString()); + } else { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); } return result; } +std::set> ModInfo::filteredMods(QString gameName, QVariantList updateData, bool addOldMods, bool markUpdated) +{ + std::set> finalMods; + for (QVariant result : updateData) { + QVariantMap update = result.toMap(); + for (auto mod : s_Collection) + if (mod->getNexusID() == update["mod_id"].toInt() && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) + if (mod->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) + finalMods.insert(mod); + } + + if (addOldMods) + for (auto mod : s_Collection) + if (mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addDays(-30) && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) + finalMods.insert(mod); + + if (markUpdated) { + std::set> updates; + for (auto mod : s_Collection) + if (mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0 && mod->canBeUpdated()) + updates.insert(mod); + std::set> diff; + std::set_difference(updates.begin(), updates.end(), finalMods.begin(), finalMods.end(), std::inserter(diff, diff.end())); + for (auto skipped : diff) { + skipped->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); + skipped->updateNXMInfo(); + } + } + return finalMods; +} int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) { diff --git a/src/modinfo.h b/src/modinfo.h index 348583d2..8c234225 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -195,6 +195,8 @@ public: **/ static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); + static std::set> filteredMods(QString gameName, QVariantList updateData, bool addOldMods = false, bool markUpdated = false); + /** * @brief create a new mod from the specified directory and add it to the collection * @param dir directory to create from diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 10a1317c..9532bf6d 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -235,10 +235,10 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re setEndorsedState(ENDORSED_FALSE); } m_LastNexusQuery = QDateTime::currentDateTimeUtc(); - m_NexusLastModified = QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt()); + m_NexusLastModified = QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC); m_MetaInfoChanged = true; saveMeta(); - disconnect(sender(), SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant))); + disconnect(sender(), SIGNAL(descriptionAvailable(QString, int, QVariant, QVariant))); emit modDetailsUpdated(true); } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index f1fa252a..abddbd3e 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -343,6 +343,26 @@ int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receive return -1; } +int NexusInterface::requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, + const QString &subModule, const MOBase::IPluginGame const *game) +{ + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; +} int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule) @@ -531,6 +551,21 @@ void NexusInterface::nextRequest() case NXMRequestInfo::TYPE_MODINFO: { url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); } break; + case NXMRequestInfo::TYPE_CHECKUPDATES: { + QString period; + switch (info.m_UpdatePeriod) { + case UpdatePeriod::DAY: + period = "1d"; + break; + case UpdatePeriod::WEEK: + period = "1w"; + break; + case UpdatePeriod::MONTH: + period = "1m"; + break; + } + url = QString("%1/games/%2/mods/updated?period=%3").arg(info.m_URL).arg(info.m_GameName).arg(period); + } break; case NXMRequestInfo::TYPE_FILES: case NXMRequestInfo::TYPE_GETUPDATES: { url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); @@ -624,6 +659,9 @@ void NexusInterface::requestFinished(std::list::iterator iter) case NXMRequestInfo::TYPE_MODINFO: { emit nxmModInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_CHECKUPDATES: { + emit nxmUpdateInfoAvailable(iter->m_GameName, iter->m_UserData, result, iter->m_ID); + } break; case NXMRequestInfo::TYPE_FILES: { emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; @@ -726,6 +764,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_FileID(0) , m_Reply(nullptr) , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) , m_UserData(userData) , m_Timeout(nullptr) , m_Reroute(false) @@ -750,6 +789,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_FileID(0) , m_Reply(nullptr) , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) , m_UserData(userData) , m_Timeout(nullptr) , m_Reroute(false) @@ -773,6 +813,30 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_FileID(fileID) , m_Reply(nullptr) , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(0) + , m_ModVersion("0") + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UpdatePeriod(period) , m_UserData(userData) , m_Timeout(nullptr) , m_Reroute(false) diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 1c283335..06b19947 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -127,6 +127,15 @@ class NexusInterface : public QObject { Q_OBJECT +public: + + enum UpdatePeriod { + NONE, + DAY, + WEEK, + MONTH + }; + public: ~NexusInterface(); @@ -202,6 +211,15 @@ public: int requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + int requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, + const QString &subModule) + { + return requestUpdateInfo(gameName, period, receiver, userData, subModule, getGame(gameName)); + } + + int requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game); + /** * @brief request nexus descriptions for multiple mods at once * @param modID id of the mod the caller is interested in @@ -378,6 +396,7 @@ signals: void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); @@ -415,8 +434,10 @@ private: TYPE_FILEINFO, TYPE_DOWNLOADURL, TYPE_TOGGLEENDORSEMENT, - TYPE_GETUPDATES + TYPE_GETUPDATES, + TYPE_CHECKUPDATES } m_Type; + UpdatePeriod m_UpdatePeriod; QVariant m_UserData; QTimer *m_Timeout; QString m_URL; @@ -430,6 +451,7 @@ private: NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(UpdatePeriod period, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); private: static QAtomicInt s_NextID; -- cgit v1.3.1 From 2b9d2f32fbbc576c9367a24c0347e5870ff20117 Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 11 Feb 2019 16:51:47 -0600 Subject: Endorsement and efficiency updates * Reduce update timeout to 5 minutes * Introduce bulk endorsement check replacing global mod info check * Fine tune performance of bulk update info query * Thread the update processing to prevent lockups --- src/mainwindow.cpp | 162 ++++++++++++++++++++++++++++++++++--------------- src/mainwindow.h | 13 ++-- src/modinfo.cpp | 37 ++++------- src/modinfo.h | 4 +- src/modinforegular.cpp | 3 + src/nexusinterface.cpp | 70 +++++++++++++++++---- src/nexusinterface.h | 9 ++- 7 files changed, 203 insertions(+), 95 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5e97a959..7592bd1e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -261,7 +261,8 @@ MainWindow::MainWindow(QSettings &initSettings actionToToolButton(ui->actionEndorseMO); createEndorseWidget(); - ui->actionEndorseMO->setVisible(false); + + toggleMO2EndorseState(); for (QAction *action : ui->toolBar->actions()) { if (action->isSeparator()) { @@ -752,7 +753,7 @@ void MainWindow::createEndorseWidget() buttonMenu->addAction(endorseAction); QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu); - connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(wontEndorse())); + connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(on_actionWontEndorseMO_triggered())); buttonMenu->addAction(wontEndorseAction); } @@ -3983,8 +3984,8 @@ void MainWindow::checkModsForUpdates() { statusBar()->show(); if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); - m_RefreshProgress->setRange(0, m_ModsToUpdate); + ModInfo::checkAllForUpdate(&m_PluginContainer, this); + NexusInterface::instance(&m_PluginContainer)->requestEndorsementInfo(this, QVariant(), QString()); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { @@ -5318,26 +5319,8 @@ void MainWindow::motdReceived(const QString &motd) m_OrganizerCore.settings().setMotDHash(hash); } } - - ui->actionEndorseMO->setVisible(false); -} - - -void MainWindow::notEndorsedYet() -{ - if (!Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { - ui->actionEndorseMO->setVisible(true); - } -} - - -void MainWindow::wontEndorse() -{ - Settings::instance().directInterface().setValue("wont_endorse_MO", true); - ui->actionEndorseMO->setVisible(false); } - void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) { QTreeWidget *dataTree = findChild("dataTree"); @@ -5398,6 +5381,21 @@ void MainWindow::on_actionEndorseMO_triggered() } } +void MainWindow::on_actionWontEndorseMO_triggered() +{ + // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now + IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); + if (!game) return; + + if (QMessageBox::question(this, tr("Abstain from Endorsing Mod Organizer"), + tr("Are you sure you want to abstain from endorsing Mod Organizer 2?\n" + "You will have to visit the mod page on the %1 Nexus site to change your mind.").arg( + NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement( + game->gameShortName(), game->nexusModOrganizerID(), m_OrganizerCore.getVersion().canonicalString(), false, this, QVariant(), QString()); + } +} void MainWindow::initDownloadView() { @@ -5448,21 +5446,10 @@ void MainWindow::updateDownloadView() m_OrganizerCore.downloadManager()->refreshList(); } -void MainWindow::modDetailsUpdated(bool) -{ - if (m_ModsToUpdate <= 0) { - statusBar()->hide(); - m_RefreshProgress->setVisible(false); - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } -} - void MainWindow::modUpdateCheck(std::multimap IDs) { if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - m_ModsToUpdate += ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs); - m_RefreshProgress->setRange(0, m_ModsToUpdate); + ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { @@ -5473,6 +5460,80 @@ void MainWindow::modUpdateCheck(std::multimap IDs) } } +void MainWindow::toggleMO2EndorseState() +{ + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionEndorseMO)); + if (Settings::instance().endorsementIntegration()) { + ui->actionEndorseMO->setVisible(true); + if (Settings::instance().directInterface().contains("endorse_state")) { + ui->actionEndorseMO->setEnabled(false); + if (Settings::instance().directInterface().value("endorse_state").toString() == "Endorsed") { + ui->actionEndorseMO->setToolTip(tr("Thank you for endorsing MO2! :)")); + toolBtn->setToolTip(tr("Thank you for endorsing MO2! :)")); + } else if (Settings::instance().directInterface().value("endorse_state").toString() == "Abstained") { + ui->actionEndorseMO->setToolTip(tr("Please reconsider endorsing MO2 on Nexus!")); + toolBtn->setToolTip(tr("Please reconsider endorsing MO2 on Nexus!")); + } + } else { + ui->actionEndorseMO->setEnabled(true); + } + } else + ui->actionEndorseMO->setVisible(false); +} + +void MainWindow::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int) +{ + QVariantList data = resultData.toList(); + std::multimap> sorted; + QStringList games = m_OrganizerCore.managedGame()->validShortNames(); + games += m_OrganizerCore.managedGame()->gameShortName(); + bool searchedMO2NexusGame = false; + for (auto endorsementData : data) { + QVariantMap endorsement = endorsementData.toMap(); + std::pair data = std::make_pair(endorsement["mod_id"].toInt(), endorsement["status"].toString()); + sorted.insert(std::pair>(endorsement["domain_name"].toString(), data)); + } + for (auto game : games) { + IPluginGame *gamePlugin = m_OrganizerCore.getGame(game); + if (gamePlugin->gameShortName().compare("SkyrimSE", Qt::CaseInsensitive) == 0) + searchedMO2NexusGame = true; + auto iter = sorted.equal_range(gamePlugin->gameNexusName()); + for (auto result = iter.first; result != iter.second; ++result) { + std::vector modsList = ModInfo::getByModID(result->first, result->second.first); + + for (auto mod : modsList) { + if (result->second.second == "Endorsed") + mod->setIsEndorsed(true); + else if (result->second.second == "Abstained") + mod->setNeverEndorse(); + else + mod->setIsEndorsed(false); + } + + if (Settings::instance().endorsementIntegration()) { + if (result->first == "skyrimspecialedition" && result->second.first == gamePlugin->nexusModOrganizerID()) { + Settings::instance().directInterface().setValue("endorse_state", result->second.second); + toggleMO2EndorseState(); + } + } + } + } + + if (!searchedMO2NexusGame && Settings::instance().endorsementIntegration()) { + auto gamePlugin = m_OrganizerCore.getGame("SkyrimSE"); + if (gamePlugin) { + auto iter = sorted.equal_range(gamePlugin->gameNexusName()); + for (auto result = iter.first; result != iter.second; ++result) { + if (result->second.first == gamePlugin->nexusModOrganizerID()) { + Settings::instance().directInterface().setValue("endorse_state", result->second.second); + toggleMO2EndorseState(); + break; + } + } + } + } +} + void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int) { QString gameNameReal; @@ -5483,7 +5544,20 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVa } } QVariantList resultList = resultData.toList(); - std::set> finalMods = ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true); + + QFutureWatcher>> *watcher = new QFutureWatcher>>(); + QObject::connect(watcher, &QFutureWatcher>>::finished, this, &MainWindow::finishUpdateInfo); + QFuture>> future = QtConcurrent::run([=]() -> std::set> { + return ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true); + }); + watcher->setFuture(future); +} + +void MainWindow::finishUpdateInfo() +{ + QFutureWatcher>> *watcher = static_cast>> *>(sender()); + + auto finalMods = watcher->result(); if (finalMods.empty()) { qInfo("None of your mods appear to have had recent file updates."); @@ -5501,6 +5575,9 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVa for (auto game : organizedGames) NexusInterface::instance(&m_PluginContainer)->requestUpdates(game.second, this, QVariant(), game.first, QString()); + + disconnect(sender()); + delete sender(); } void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -5508,7 +5585,6 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD QVariantMap resultInfo = resultData.toMap(); QList files = resultInfo["files"].toList(); QList fileUpdates = resultInfo["file_updates"].toList(); - m_ModsToUpdate--; QString gameNameReal; for (IPluginGame *game : m_PluginContainer.plugins()) { if (game->gameNexusName() == gameName) { @@ -5578,19 +5654,10 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD // Scrape mod data here so we can use the mod version if no file update was located requiresInfo = true; } - - if (mod->getLastNexusQuery().addDays(1) <= QDateTime::currentDateTimeUtc()) - requiresInfo = true; } if (requiresInfo) NexusInterface::instance(&m_PluginContainer)->requestModInfo(gameName, modID, this, QVariant(), QString()); - - if (--m_ModsToUpdate <= 0) { - statusBar()->hide(); - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } } void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -5663,11 +5730,6 @@ void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultDat void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, QNetworkReply::NetworkError error, const QString &errorString) { - if (modID == -1) { - // must be the update-check that failed - m_ModsToUpdate = 0; - statusBar()->hide(); - } if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) { QString gameNameReal; for (IPluginGame *game : m_PluginContainer.plugins()) { diff --git a/src/mainwindow.h b/src/mainwindow.h index d2704b72..a0c043a1 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -64,6 +64,7 @@ namespace MOShared { class DirectoryEntry; } #include #include #include +#include class QAction; class QAbstractItemModel; @@ -303,6 +304,8 @@ private: void sendSelectedModsToPriority(int newPriority); void sendSelectedPluginsToPriority(int newPriority); + void toggleMO2EndorseState(); + private: static const char *PATTERN_BACKUP_GLOB; @@ -341,8 +344,6 @@ private: CategoryFactory &m_CategoryFactory; - int m_ModsToUpdate; - bool m_LoginAttempted; QTimer m_CheckBSATimer; @@ -494,8 +495,6 @@ private slots: void updateAvailable(); void motdReceived(const QString &motd); - void notEndorsedYet(); - void wontEndorse(); void originModified(int originID); @@ -504,12 +503,13 @@ private slots: void addPrimaryCategoryCandidates(); - void modDetailsUpdated(bool success); - void modInstalled(const QString &modName); void modUpdateCheck(std::multimap IDs); + void finishUpdateInfo(); + + void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int); void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); @@ -622,6 +622,7 @@ private slots: // ui slots void on_actionSettings_triggered(); void on_actionUpdate_triggered(); void on_actionEndorseMO_triggered(); + void on_actionWontEndorseMO_triggered(); void on_bsaList_customContextMenuRequested(const QPoint &pos); void on_clearFiltersButton_clicked(); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 3e694baf..938a6418 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -284,16 +284,8 @@ ModInfo::ModInfo(PluginContainer *pluginContainer) } -int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) +void ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) { - int result = 0; - - // MO2 endorsement status is no longer available via this method - an alternative must be found - //IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition"); - //if (game && game->nexusModOrganizerID()) { - // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); - //} - QDateTime earliest = QDateTime::currentDateTimeUtc(); QDateTime latest; std::set games; @@ -315,8 +307,6 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } } - result = organizedGames.size(); - if (organizedGames.empty()) qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); @@ -336,8 +326,6 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv for (auto gameName : games) NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); } - - return result; } std::set> ModInfo::filteredMods(QString gameName, QVariantList updateData, bool addOldMods, bool markUpdated) @@ -345,10 +333,12 @@ std::set> ModInfo::filteredMods(QString gameName, QVaria std::set> finalMods; for (QVariant result : updateData) { QVariantMap update = result.toMap(); - for (auto mod : s_Collection) - if (mod->getNexusID() == update["mod_id"].toInt() && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) - if (mod->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) - finalMods.insert(mod); + std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(finalMods, finalMods.end()), [=](QSharedPointer info) -> bool { + if (info->getNexusID() == update["mod_id"].toInt() && info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) + if (info->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) + return true; + return false; + }); } if (addOldMods) @@ -358,20 +348,21 @@ std::set> ModInfo::filteredMods(QString gameName, QVaria if (markUpdated) { std::set> updates; - for (auto mod : s_Collection) - if (mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0 && mod->canBeUpdated()) - updates.insert(mod); + std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(updates, updates.end()), [=](QSharedPointer info) -> bool { + if (info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0 && info->canBeUpdated()) + return true; + return false; + }); std::set> diff; std::set_difference(updates.begin(), updates.end(), finalMods.begin(), finalMods.end(), std::inserter(diff, diff.end())); for (auto skipped : diff) { skipped->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); - skipped->updateNXMInfo(); } } return finalMods; } -int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) +void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) { std::vector> mods; std::set> organizedGames; @@ -402,8 +393,6 @@ int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiv } else { qInfo("None of the selected mods can be updated."); } - - return organizedGames.size(); } diff --git a/src/modinfo.h b/src/modinfo.h index 8c234225..c3115685 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -188,12 +188,12 @@ public: /** * @brief run a limited batch of mod update checks for "newest version" information */ - static int manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs); + static void manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs); /** * @brief query nexus information for every mod and update the "newest version" information **/ - static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); + static void checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); static std::set> filteredMods(QString gameName, QVariantList updateData, bool addOldMods = false, bool markUpdated = false); diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 9532bf6d..ac1c46d6 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -507,6 +507,8 @@ bool ModInfoRegular::canBeUpdated() const QDateTime ModInfoRegular::getExpires() const { + return m_LastNexusUpdate.addSecs(300); + /* switch (Settings::instance().nexusUpdateStrategy()) { case Settings::NexusUpdateStrategy::Flexible: { qint64 diff = m_NexusLastModified.msecsTo(QDateTime::currentDateTimeUtc()); @@ -530,6 +532,7 @@ QDateTime ModInfoRegular::getExpires() const return m_LastNexusUpdate.addSecs(86400); } break; } + */ } std::vector ModInfoRegular::getFlags() const diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index abddbd3e..e657dfc1 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -125,6 +125,15 @@ void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int file } } +void NexusBridge::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit endorsementsAvailable(userData, resultData); + } +} + void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { std::set::iterator iter = m_RequestIDs.find(requestID); @@ -455,6 +464,21 @@ int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, return requestInfo.m_ID; } +int NexusInterface::requestEndorsementInfo(QObject *receiver, QVariant userData, const QString &subModule) +{ + NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_ENDORSEMENTS, userData, subModule); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmEndorsementsAvailable(QVariant, QVariant, int)), + receiver, SLOT(nxmEndorsementsAvailable(QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) @@ -478,12 +502,6 @@ int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QStrin return -1; } -bool NexusInterface::requiresLogin(const NXMRequestInfo &info) -{ - return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT) - || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL); -} - IPluginGame* NexusInterface::getGame(QString gameName) const { auto gamePlugins = m_PluginContainer->plugins(); @@ -516,7 +534,7 @@ void NexusInterface::nextRequest() return; } - if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { + if (!getAccessManager()->validated()) { if (!getAccessManager()->validateAttempted()) { emit needLogin(); return; @@ -581,6 +599,9 @@ void NexusInterface::nextRequest() else url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); } break; + case NXMRequestInfo::TYPE_ENDORSEMENTS: { + url = QString("%1/user/endorsements").arg(info.m_URL); + } break; case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { QString endorse = info.m_Endorse ? "endorse" : "abstain"; url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse); @@ -674,6 +695,9 @@ void NexusInterface::requestFinished(std::list::iterator iter) case NXMRequestInfo::TYPE_DOWNLOADURL: { emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_ENDORSEMENTS: { + emit nxmEndorsementsAvailable(iter->m_UserData, result, iter->m_ID); + } break; case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; @@ -747,7 +771,7 @@ void NexusInterface::requestTimeout() } namespace { - QString get_management_url(MOBase::IPluginGame const *game) + QString get_management_url() { return "https://api.nexusmods.com/v1"; } @@ -769,7 +793,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) @@ -794,7 +818,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) @@ -818,13 +842,35 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) , m_Endorse(false) {} +NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type + , QVariant userData + , const QString &subModule +) + : m_ModID(0) + , m_ModVersion("0") + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url()) + , m_SubModule(subModule) + , m_NexusGameID(0) + , m_GameName("") + , m_Endorse(false) +{} + + NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period , NexusInterface::NXMRequestInfo::Type type , QVariant userData @@ -841,7 +887,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 06b19947..ac9e466b 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -104,6 +104,7 @@ public slots: void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage); @@ -298,6 +299,10 @@ public: **/ int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + + int requestEndorsementInfo(QObject *receiver, QVariant userData, const QString &subModule); + + /** * @param gameName the game short name to support multiple game sources * @brief toggle endorsement state of the mod @@ -401,6 +406,7 @@ signals: void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); void requestsChanged(int queueCount, std::tuple requestsRemaining); @@ -433,6 +439,7 @@ private: TYPE_FILES, TYPE_FILEINFO, TYPE_DOWNLOADURL, + TYPE_ENDORSEMENTS, TYPE_TOGGLEENDORSEMENT, TYPE_GETUPDATES, TYPE_CHECKUPDATES @@ -451,6 +458,7 @@ private: NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(Type type, QVariant userData, const QString &subModule); NXMRequestInfo(UpdatePeriod period, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); private: @@ -464,7 +472,6 @@ private: NexusInterface(PluginContainer *pluginContainer); void nextRequest(); void requestFinished(std::list::iterator iter); - bool requiresLogin(const NXMRequestInfo &info); MOBase::IPluginGame *getGame(QString gameName) const; QString getOldModsURL(QString gameName) const; -- cgit v1.3.1 From f7d1ce1fae80e42141198547d6c54689e085327f Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 11 Feb 2019 20:30:45 -0600 Subject: Small updates, cleanup, update code, cache settings, tooltips --- src/CMakeLists.txt | 2 - src/json.cpp | 522 ----------------------------------------------- src/json.h | 94 --------- src/mainwindow.cpp | 22 +- src/mainwindow.h | 5 +- src/mainwindow.ui | 4 +- src/modinforegular.cpp | 25 --- src/modlist.cpp | 12 +- src/nexusinterface.cpp | 14 +- src/nxmaccessmanager.cpp | 53 ++--- src/settings.cpp | 14 -- src/settings.h | 16 -- src/settingsdialog.ui | 10 - 13 files changed, 65 insertions(+), 728 deletions(-) delete mode 100644 src/json.cpp delete mode 100644 src/json.h (limited to 'src/nexusinterface.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cc7d7a78..b241d99a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -79,7 +79,6 @@ SET(organizer_SRCS previewgenerator.cpp previewdialog.cpp aboutdialog.cpp - json.cpp modflagicondelegate.cpp genericicondelegate.cpp organizerproxy.cpp @@ -173,7 +172,6 @@ SET(organizer_HDRS previewgenerator.h previewdialog.h aboutdialog.h - json.h modflagicondelegate.h genericicondelegate.h organizerproxy.h diff --git a/src/json.cpp b/src/json.cpp deleted file mode 100644 index 05a2665a..00000000 --- a/src/json.cpp +++ /dev/null @@ -1,522 +0,0 @@ -/** - * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and vice-versa. - * Copyright (C) 2011 Eeli Reilin - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/** - * \file json.cpp - */ - -#include "json.h" - -namespace QtJson { - static QString sanitizeString(QString str); - static QByteArray join(const QList &list, const QByteArray &sep); - static QVariant parseValue(const QString &json, int &index, bool &success); - static QVariant parseObject(const QString &json, int &index, bool &success); - static QVariant parseArray(const QString &json, int &index, bool &success); - static QVariant parseString(const QString &json, int &index, bool &success); - static QVariant parseNumber(const QString &json, int &index); - static int lastIndexOfNumber(const QString &json, int index); - static void eatWhitespace(const QString &json, int &index); - static int lookAhead(const QString &json, int index); - static int nextToken(const QString &json, int &index); - - template - QByteArray serializeMap(const T &map, bool &success) { - QByteArray str = "{ "; - QList pairs; - for (typename T::const_iterator it = map.begin(), itend = map.end(); it != itend; ++it) { - QByteArray serializedValue = serialize(it.value()); - if (serializedValue.isNull()) { - success = false; - break; - } - pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; - } - - str += join(pairs, ", "); - str += " }"; - return str; - } - - - /** - * parse - */ - QVariant parse(const QString &json) { - bool success = true; - return parse(json, success); - } - - /** - * parse - */ - QVariant parse(const QString &json, bool &success) { - success = true; - - // Return an empty QVariant if the JSON data is either null or empty - if (!json.isNull() || !json.isEmpty()) { - QString data = json; - // We'll start from index 0 - int index = 0; - - // Parse the first value - QVariant value = parseValue(data, index, success); - - // Return the parsed value - return value; - } else { - // Return the empty QVariant - return QVariant(); - } - } - - QByteArray serialize(const QVariant &data) { - bool success = true; - return serialize(data, success); - } - - QByteArray serialize(const QVariant &data, bool &success) { - QByteArray str; - success = true; - - if (!data.isValid()) { // invalid or null? - str = "null"; - } else if ((data.type() == QVariant::List) || - (data.type() == QVariant::StringList)) { // variant is a list? - QList values; - const QVariantList list = data.toList(); - Q_FOREACH(const QVariant& v, list) { - QByteArray serializedValue = serialize(v); - if (serializedValue.isNull()) { - success = false; - break; - } - values << serializedValue; - } - - str = "[ " + join( values, ", " ) + " ]"; - } else if (data.type() == QVariant::Hash) { // variant is a hash? - str = serializeMap<>(data.toHash(), success); - } else if (data.type() == QVariant::Map) { // variant is a map? - str = serializeMap<>(data.toMap(), success); - } else if ((data.type() == QVariant::String) || - (data.type() == QVariant::ByteArray)) {// a string or a byte array? - str = sanitizeString(data.toString()).toUtf8(); - } else if (data.type() == QVariant::Double) { // double? - double value = data.toDouble(); - if ((value - value) == 0.0) { - str = QByteArray::number(value, 'g'); - if (!str.contains(".") && ! str.contains("e")) { - str += ".0"; - } - } else { - success = false; - } - } else if (data.type() == QVariant::Bool) { // boolean value? - str = data.toBool() ? "true" : "false"; - } else if (data.type() == QVariant::ULongLong) { // large unsigned number? - str = QByteArray::number(data.value()); - } else if (data.canConvert()) { // any signed number? - str = QByteArray::number(data.value()); - } else if (data.canConvert()) { //TODO: this code is never executed - str = QString::number(data.value()).toUtf8(); - } else if (data.canConvert()) { // can value be converted to string? - // this will catch QDate, QDateTime, QUrl, ... - str = sanitizeString(data.toString()).toUtf8(); - } else { - success = false; - } - - if (success) { - return str; - } else { - return QByteArray(); - } - } - - QString serializeStr(const QVariant &data) { - return QString::fromUtf8(serialize(data)); - } - - QString serializeStr(const QVariant &data, bool &success) { - return QString::fromUtf8(serialize(data, success)); - } - - - /** - * \enum JsonToken - */ - enum JsonToken { - JsonTokenNone = 0, - JsonTokenCurlyOpen = 1, - JsonTokenCurlyClose = 2, - JsonTokenSquaredOpen = 3, - JsonTokenSquaredClose = 4, - JsonTokenColon = 5, - JsonTokenComma = 6, - JsonTokenString = 7, - JsonTokenNumber = 8, - JsonTokenTrue = 9, - JsonTokenFalse = 10, - JsonTokenNull = 11 - }; - - static QString sanitizeString(QString str) { - str.replace(QLatin1String("\\"), QLatin1String("\\\\")); - str.replace(QLatin1String("\""), QLatin1String("\\\"")); - str.replace(QLatin1String("\b"), QLatin1String("\\b")); - str.replace(QLatin1String("\f"), QLatin1String("\\f")); - str.replace(QLatin1String("\n"), QLatin1String("\\n")); - str.replace(QLatin1String("\r"), QLatin1String("\\r")); - str.replace(QLatin1String("\t"), QLatin1String("\\t")); - return QString(QLatin1String("\"%1\"")).arg(str); - } - - static QByteArray join(const QList &list, const QByteArray &sep) { - QByteArray res; - Q_FOREACH(const QByteArray &i, list) { - if (!res.isEmpty()) { - res += sep; - } - res += i; - } - return res; - } - - /** - * parseValue - */ - static QVariant parseValue(const QString &json, int &index, bool &success) { - // Determine what kind of data we should parse by - // checking out the upcoming token - switch(lookAhead(json, index)) { - case JsonTokenString: - return parseString(json, index, success); - case JsonTokenNumber: - return parseNumber(json, index); - case JsonTokenCurlyOpen: - return parseObject(json, index, success); - case JsonTokenSquaredOpen: - return parseArray(json, index, success); - case JsonTokenTrue: - nextToken(json, index); - return QVariant(true); - case JsonTokenFalse: - nextToken(json, index); - return QVariant(false); - case JsonTokenNull: - nextToken(json, index); - return QVariant(); - case JsonTokenNone: - break; - } - - // If there were no tokens, flag the failure and return an empty QVariant - success = false; - return QVariant(); - } - - /** - * parseObject - */ - static QVariant parseObject(const QString &json, int &index, bool &success) { - QVariantMap map; - int token; - - // Get rid of the whitespace and increment index - nextToken(json, index); - - // Loop through all of the key/value pairs of the object - bool done = false; - while (!done) { - // Get the upcoming token - token = lookAhead(json, index); - - if (token == JsonTokenNone) { - success = false; - return QVariantMap(); - } else if (token == JsonTokenComma) { - nextToken(json, index); - } else if (token == JsonTokenCurlyClose) { - nextToken(json, index); - return map; - } else { - // Parse the key/value pair's name - QString name = parseString(json, index, success).toString(); - - if (!success) { - return QVariantMap(); - } - - // Get the next token - token = nextToken(json, index); - - // If the next token is not a colon, flag the failure - // return an empty QVariant - if (token != JsonTokenColon) { - success = false; - return QVariant(QVariantMap()); - } - - // Parse the key/value pair's value - QVariant value = parseValue(json, index, success); - - if (!success) { - return QVariantMap(); - } - - // Assign the value to the key in the map - map[name] = value; - } - } - - // Return the map successfully - return QVariant(map); - } - - /** - * parseArray - */ - static QVariant parseArray(const QString &json, int &index, bool &success) { - QVariantList list; - - nextToken(json, index); - - bool done = false; - while(!done) { - int token = lookAhead(json, index); - - if (token == JsonTokenNone) { - success = false; - return QVariantList(); - } else if (token == JsonTokenComma) { - nextToken(json, index); - } else if (token == JsonTokenSquaredClose) { - nextToken(json, index); - break; - } else { - QVariant value = parseValue(json, index, success); - if (!success) { - return QVariantList(); - } - list.push_back(value); - } - } - - return QVariant(list); - } - - /** - * parseString - */ - static QVariant parseString(const QString &json, int &index, bool &success) { - QString s; - QChar c; - - eatWhitespace(json, index); - - c = json[index++]; - - bool complete = false; - while(!complete) { - if (index == json.size()) { - break; - } - - c = json[index++]; - - if (c == '\"') { - complete = true; - break; - } else if (c == '\\') { - if (index == json.size()) { - break; - } - - c = json[index++]; - - if (c == '\"') { - s.append('\"'); - } else if (c == '\\') { - s.append('\\'); - } else if (c == '/') { - s.append('/'); - } else if (c == 'b') { - s.append('\b'); - } else if (c == 'f') { - s.append('\f'); - } else if (c == 'n') { - s.append('\n'); - } else if (c == 'r') { - s.append('\r'); - } else if (c == 't') { - s.append('\t'); - } else if (c == 'u') { - int remainingLength = json.size() - index; - if (remainingLength >= 4) { - QString unicodeStr = json.mid(index, 4); - - int symbol = unicodeStr.toInt(0, 16); - - s.append(QChar(symbol)); - - index += 4; - } else { - break; - } - } - } else { - s.append(c); - } - } - - if (!complete) { - success = false; - return QVariant(); - } - - return QVariant(s); - } - - /** - * parseNumber - */ - static QVariant parseNumber(const QString &json, int &index) { - eatWhitespace(json, index); - - int lastIndex = lastIndexOfNumber(json, index); - int charLength = (lastIndex - index) + 1; - QString numberStr; - - numberStr = json.mid(index, charLength); - - index = lastIndex + 1; - bool ok; - - if (numberStr.contains('.')) { - return QVariant(numberStr.toDouble(nullptr)); - } else if (numberStr.startsWith('-')) { - int i = numberStr.toInt(&ok); - if (!ok) { - qlonglong ll = numberStr.toLongLong(&ok); - return ok ? ll : QVariant(numberStr); - } - return i; - } else { - uint u = numberStr.toUInt(&ok); - if (!ok) { - qulonglong ull = numberStr.toULongLong(&ok); - return ok ? ull : QVariant(numberStr); - } - return u; - } - } - - /** - * lastIndexOfNumber - */ - static int lastIndexOfNumber(const QString &json, int index) { - int lastIndex; - - for(lastIndex = index; lastIndex < json.size(); lastIndex++) { - if (QString("0123456789+-.eE").indexOf(json[lastIndex]) == -1) { - break; - } - } - - return lastIndex -1; - } - - /** - * eatWhitespace - */ - static void eatWhitespace(const QString &json, int &index) { - for(; index < json.size(); index++) { - if (QString(" \t\n\r").indexOf(json[index]) == -1) { - break; - } - } - } - - /** - * lookAhead - */ - static int lookAhead(const QString &json, int index) { - int saveIndex = index; - return nextToken(json, saveIndex); - } - - /** - * nextToken - */ - static int nextToken(const QString &json, int &index) { - eatWhitespace(json, index); - - if (index == json.size()) { - return JsonTokenNone; - } - - QChar c = json[index]; - index++; - switch(c.toLatin1()) { - case '{': return JsonTokenCurlyOpen; - case '}': return JsonTokenCurlyClose; - case '[': return JsonTokenSquaredOpen; - case ']': return JsonTokenSquaredClose; - case ',': return JsonTokenComma; - case '"': return JsonTokenString; - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - case '-': return JsonTokenNumber; - case ':': return JsonTokenColon; - } - index--; // ^ WTF? - - int remainingLength = json.size() - index; - - // True - if (remainingLength >= 4) { - if (json[index] == 't' && json[index + 1] == 'r' && - json[index + 2] == 'u' && json[index + 3] == 'e') { - index += 4; - return JsonTokenTrue; - } - } - - // False - if (remainingLength >= 5) { - if (json[index] == 'f' && json[index + 1] == 'a' && - json[index + 2] == 'l' && json[index + 3] == 's' && - json[index + 4] == 'e') { - index += 5; - return JsonTokenFalse; - } - } - - // Null - if (remainingLength >= 4) { - if (json[index] == 'n' && json[index + 1] == 'u' && - json[index + 2] == 'l' && json[index + 3] == 'l') { - index += 4; - return JsonTokenNull; - } - } - - return JsonTokenNone; - } -} //end namespace diff --git a/src/json.h b/src/json.h deleted file mode 100644 index 57842e37..00000000 --- a/src/json.h +++ /dev/null @@ -1,94 +0,0 @@ -/** - * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and vice-versa. - * Copyright (C) 2011 Eeli Reilin - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/** - * \file json.h - */ - -#ifndef JSON_H -#define JSON_H - -#include -#include - - -/** - * \namespace QtJson - * \brief A JSON data parser - * - * Json parses a JSON data into a QVariant hierarchy. - */ -namespace QtJson { - typedef QVariantMap JsonObject; - typedef QVariantList JsonArray; - - /** - * Parse a JSON string - * - * \param json The JSON data - */ - QVariant parse(const QString &json); - - /** - * Parse a JSON string - * - * \param json The JSON data - * \param success The success of the parsing - */ - QVariant parse(const QString &json, bool &success); - - /** - * This method generates a textual JSON representation - * - * \param data The JSON data generated by the parser. - * - * \return QByteArray Textual JSON representation in UTF-8 - */ - QByteArray serialize(const QVariant &data); - - /** - * This method generates a textual JSON representation - * - * \param data The JSON data generated by the parser. - * \param success The success of the serialization - * - * \return QByteArray Textual JSON representation in UTF-8 - */ - QByteArray serialize(const QVariant &data, bool &success); - - /** - * This method generates a textual JSON representation - * - * \param data The JSON data generated by the parser. - * - * \return QString Textual JSON representation - */ - QString serializeStr(const QVariant &data); - - /** - * This method generates a textual JSON representation - * - * \param data The JSON data generated by the parser. - * \param success The success of the serialization - * - * \return QString Textual JSON representation - */ - QString serializeStr(const QVariant &data, bool &success); -} - -#endif //JSON_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7592bd1e..1ecb6417 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -749,11 +749,11 @@ void MainWindow::createEndorseWidget() buttonMenu->clear(); QAction *endorseAction = new QAction(tr("Endorse"), buttonMenu); - connect(endorseAction, SIGNAL(triggered()), this, SLOT(on_actionEndorseMO_triggered())); + connect(endorseAction, SIGNAL(triggered()), this, SLOT(actionEndorseMO())); buttonMenu->addAction(endorseAction); QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu); - connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(on_actionWontEndorseMO_triggered())); + connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(actionWontEndorseMO())); buttonMenu->addAction(wontEndorseAction); } @@ -1866,9 +1866,21 @@ void MainWindow::processUpdates() { lastHidden = hidden; } } - if (lastVersion < QVersionNumber(2,1,6)) { + if (lastVersion < QVersionNumber(2, 1, 6)) { ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true); } + if (lastVersion < QVersionNumber(2, 2, 0)) { + QSettings &instance = Settings::instance().directInterface(); + instance.beginGroup("Settings"); + instance.remove("steam_password"); + instance.remove("nexus_username"); + instance.remove("nexus_password"); + instance.remove("nexus_api_key"); + instance.endGroup(); + instance.beginGroup("Servers"); + instance.remove(""); + instance.endGroup(); + } } if (currentVersion > lastVersion) { @@ -5366,7 +5378,7 @@ void MainWindow::on_actionUpdate_triggered() } -void MainWindow::on_actionEndorseMO_triggered() +void MainWindow::actionEndorseMO() { // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); @@ -5381,7 +5393,7 @@ void MainWindow::on_actionEndorseMO_triggered() } } -void MainWindow::on_actionWontEndorseMO_triggered() +void MainWindow::actionWontEndorseMO() { // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); diff --git a/src/mainwindow.h b/src/mainwindow.h index a0c043a1..ff23b8fb 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -494,6 +494,9 @@ private slots: void updateAvailable(); + void actionEndorseMO(); + void actionWontEndorseMO(); + void motdReceived(const QString &motd); void originModified(int originID); @@ -621,8 +624,6 @@ private slots: // ui slots void on_actionNotifications_triggered(); void on_actionSettings_triggered(); void on_actionUpdate_triggered(); - void on_actionEndorseMO_triggered(); - void on_actionWontEndorseMO_triggered(); void on_bsaList_customContextMenuRequested(const QPoint &pos); void on_clearFiltersButton_clicked(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index ad6b3e38..5ac8f092 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -550,10 +550,10 @@ p, li { white-space: pre-wrap; } - API Queued and Remaining Requests + Nexus API Queued and Remaining Requests - <html><head/><body><p>This tracks the number of queued Nexus API requests on the left (<span style=" font-weight:600;">Q</span>) and the remaining daily (<span style=" font-weight:600;">D</span>) and hourly (<span style=" font-weight:600;">H</span>) requests on the right. The Nexus API limits you to a pool of <span style=" font-weight:600;">2500</span> requests per day and <span style=" font-weight:600;">100</span> requests per hour. It is dynamically updated every time a request is completed. If you exceed this limit, you will be unable to queue downloads, check updates, parse mod info, or even log in.</p></body></html> + <html><head/><body><p>This tracks the number of queued Nexus API requests on the left (<span style=" font-weight:600;">Q</span>) and the remaining daily (<span style=" font-weight:600;">D</span>) and hourly (<span style=" font-weight:600;">H</span>) requests on the right. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens.</p></body></html> QFrame::StyledPanel diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index ac1c46d6..8bef1c28 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -508,31 +508,6 @@ bool ModInfoRegular::canBeUpdated() const QDateTime ModInfoRegular::getExpires() const { return m_LastNexusUpdate.addSecs(300); - /* - switch (Settings::instance().nexusUpdateStrategy()) { - case Settings::NexusUpdateStrategy::Flexible: { - qint64 diff = m_NexusLastModified.msecsTo(QDateTime::currentDateTimeUtc()); - qint64 year = 31536000000; - qint64 sixMonths = 15768000000; - qint64 threeMonths = 7884000000; - qint64 oneMonth = 1314000000; - - if (diff < oneMonth) - return m_LastNexusUpdate.addSecs(7200); - else if (diff < threeMonths) - return m_LastNexusUpdate.addSecs(14400); - else if (diff < sixMonths) - return m_LastNexusUpdate.addSecs(21600); - else if (diff < year) - return m_LastNexusUpdate.addSecs(43200); - else - return m_LastNexusUpdate.addSecs(86400); - } break; - case Settings::NexusUpdateStrategy::Rigid: { - return m_LastNexusUpdate.addSecs(86400); - } break; - } - */ } std::vector ModInfoRegular::getFlags() const diff --git a/src/modlist.cpp b/src/modlist.cpp index 55241492..bb490113 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -466,15 +466,11 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const if (modInfo->getNexusID() > 0) { if (!modInfo->canBeUpdated()) { qint64 remains = QDateTime::currentDateTimeUtc().secsTo(modInfo->getExpires()); - qint64 hours = remains / 3600; - qint64 minutes = (remains % 3600) / 60; - QString remainsStr(tr("%1 hours and %2 minutes").arg(hours).arg(minutes)); - text += "
    " + tr("This mod was last checked on %1. It will be available to check in %2.") - .arg(modInfo->getLastNexusUpdate().toLocalTime().toString(Qt::DefaultLocaleShortDate)) + qint64 minutes = remains / 60; + qint64 seconds = (remains % 60) / 60; + QString remainsStr(tr("%1 minute(s) and %2 second(s)").arg(minutes).arg(seconds)); + text += "
    " + tr("This mod will be available to check in %2.") .arg(remainsStr); - } else { - text += "
    " + tr("This mod is eligible for an update check."); - text += "
    " + tr("It was last checked on %1").arg(modInfo->getLastNexusUpdate().toLocalTime().toString(Qt::DefaultLocaleShortDate)); } } return text; diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index e657dfc1..265d898c 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -21,7 +21,6 @@ along with Mod Organizer. If not, see . #include "iplugingame.h" #include "nxmaccessmanager.h" -#include "json.h" #include "selectiondialog.h" #include "bbcode.h" #include @@ -613,6 +612,8 @@ void NexusInterface::nextRequest() url = info.m_URL; } QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); request.setRawHeader("APIKEY", m_AccessManager->apiKey().toUtf8()); request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule)); request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); @@ -645,7 +646,10 @@ void NexusInterface::requestFinished(std::list::iterator iter) if (reply->error() != QNetworkReply::NoError) { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (statusCode == 429) { - qWarning("All API requests have been consumed and are now being denied."); + if (reply->rawHeader("x-rl-daily-remaining").toInt() || reply->rawHeader("x-rl-hourly-remaining").toInt()) + qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team."); + else + qWarning("All API requests have been consumed and are now being denied."); qWarning("Error: %s", reply->errorString().toUtf8().constData()); } else { qWarning("request failed: %s", reply->errorString().toUtf8().constData()); @@ -670,9 +674,9 @@ void NexusInterface::requestFinished(std::list::iterator iter) qDebug("nexus error: %s", qUtf8Printable(nexusError)); emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), nexusError); } else { - bool ok; - QVariant result = QtJson::parse(data, ok); - if (result.isValid() && ok) { + QJsonDocument responseDoc = QJsonDocument::fromJson(data); + if (!responseDoc.isNull()) { + QVariant result = responseDoc.toVariant(); switch (iter->m_Type) { case NXMRequestInfo::TYPE_DESCRIPTION: { emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 1e0ac49b..aca1d785 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -259,33 +259,40 @@ void NXMAccessManager::validateFinished() if (m_ValidateReply != nullptr) { QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll()); - QJsonObject credentialsData = jdoc.object(); - if (credentialsData.contains("user_id")) { - QString name = credentialsData.value("name").toString(); - bool premium = credentialsData.value("is_premium").toBool(); - - std::tuple limits(std::make_tuple( - m_ValidateReply->rawHeader("x-rl-daily-remaining").toInt(), - m_ValidateReply->rawHeader("x-rl-daily-limit").toInt(), - m_ValidateReply->rawHeader("x-rl-hourly-remaining").toInt(), - m_ValidateReply->rawHeader("x-rl-hourly-limit").toInt() - )); - - emit credentialsReceived(name, premium, limits); - - m_ValidateReply->deleteLater(); - m_ValidateReply = nullptr; - - m_ValidateState = VALIDATE_VALID; - emit validateSuccessful(true); + if (!jdoc.isNull()) { + QJsonObject credentialsData = jdoc.object(); + if (credentialsData.contains("user_id")) { + QString name = credentialsData.value("name").toString(); + bool premium = credentialsData.value("is_premium").toBool(); + + std::tuple limits(std::make_tuple( + m_ValidateReply->rawHeader("x-rl-daily-remaining").toInt(), + m_ValidateReply->rawHeader("x-rl-daily-limit").toInt(), + m_ValidateReply->rawHeader("x-rl-hourly-remaining").toInt(), + m_ValidateReply->rawHeader("x-rl-hourly-limit").toInt() + )); + + emit credentialsReceived(name, premium, limits); + + m_ValidateReply->deleteLater(); + m_ValidateReply = nullptr; + + m_ValidateState = VALIDATE_VALID; + emit validateSuccessful(true); + } else { + m_ApiKey.clear(); + m_ValidateState = VALIDATE_NOT_VALID; + emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString())); + } } else { m_ApiKey.clear(); - m_ValidateState = VALIDATE_NOT_VALID; - emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString())); + m_ValidateState = VALIDATE_NOT_CHECKED; + emit validateFailed(tr("Could not parse response. Invalid JSON.")); } - } else { + } + else { m_ApiKey.clear(); m_ValidateState = VALIDATE_NOT_CHECKED; - emit validateFailed(tr("unknown error")); + emit validateFailed(tr("Unknown error.")); } } diff --git a/src/settings.cpp b/src/settings.cpp index 64a39e12..7b4b3a67 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -92,8 +92,6 @@ Settings::Settings(const QSettings &settingsSource) } else { s_Instance = this; } - - qRegisterMetaType("NexusUpdateStrategy"); } @@ -416,11 +414,6 @@ int Settings::crashDumpsMax() const return m_Settings.value("Settings/crash_dumps_max", 5).toInt(); } -Settings::NexusUpdateStrategy Settings::nexusUpdateStrategy() const -{ - return static_cast(m_Settings.value("Settings/nexus_update_strategy", std::rand() / ((RAND_MAX + 1u) / 2)).toInt()); -} - QColor Settings::modlistOverwrittenLooseColor() const { return m_Settings.value("Settings/overwrittenLooseFilesColor", QColor(0, 255, 0, 64)).value(); @@ -1042,10 +1035,7 @@ Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) , m_preferredServersList( dialog.findChild("preferredServersList")) , m_endorsementBox(dialog.findChild("endorsementBox")) - , m_updateStrategyBox(dialog.findChild("updateStrategy")) { - qRegisterMetaType("NexusUpdateStrategy"); - if (!deObfuscate("APIKEY").isEmpty()) { m_nexusConnect->setText("Nexus API Key Stored"); m_nexusConnect->setDisabled(true); @@ -1054,8 +1044,6 @@ Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) m_offlineBox->setChecked(parent->offlineMode()); m_proxyBox->setChecked(parent->useProxy()); m_endorsementBox->setChecked(parent->endorsementIntegration()); - if (parent->nexusUpdateStrategy() == Settings::NexusUpdateStrategy::Flexible) - m_updateStrategyBox->setChecked(true); // display server preferences @@ -1101,8 +1089,6 @@ void Settings::NexusTab::update() m_Settings.setValue("Settings/offline_mode", m_offlineBox->isChecked()); m_Settings.setValue("Settings/use_proxy", m_proxyBox->isChecked()); m_Settings.setValue("Settings/endorsement_integration", m_endorsementBox->isChecked()); - m_Settings.setValue("Settings/nexus_update_strategy", m_updateStrategyBox->isChecked() - ? Settings::NexusUpdateStrategy::Flexible : Settings::NexusUpdateStrategy::Rigid); // store server preference m_Settings.beginGroup("Servers"); diff --git a/src/settings.h b/src/settings.h index ddb7cac0..2cd3f0b6 100644 --- a/src/settings.h +++ b/src/settings.h @@ -65,14 +65,6 @@ class Settings : public QObject { Q_OBJECT - Q_ENUMS(NexusUpdateStrategy) - -public: - - enum NexusUpdateStrategy { - Rigid, - Flexible - }; public: @@ -239,11 +231,6 @@ public: */ int crashDumpsMax() const; - /** - * @return the configured Nexus update strategy - */ - NexusUpdateStrategy nexusUpdateStrategy() const; - QColor modlistOverwrittenLooseColor() const; QColor modlistOverwritingLooseColor() const; @@ -499,7 +486,6 @@ private: QListWidget *m_knownServersList; QListWidget *m_preferredServersList; QCheckBox *m_endorsementBox; - QCheckBox *m_updateStrategyBox; }; /** Display/store the configuration in the 'steam' tab of the settings dialogue */ @@ -578,6 +564,4 @@ private: }; -Q_DECLARE_METATYPE(Settings::NexusUpdateStrategy) - #endif // SETTINGS_H diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index cfbcf429..127c94c7 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -578,16 +578,6 @@ p, li { white-space: pre-wrap; }
    - - - - <html><head/><body><p>Rather than a global <span style=" font-weight:600;">1 day</span> cache, update caches will expire at different rates depending on how recently a mod has been updated on Nexus.</p><p><br/></p><p>Less than a month: <span style=" font-style:italic;">2 hours</span></p><p>Less than three months: <span style=" font-style:italic;">4 hours</span></p><p>Less than six months: <span style=" font-style:italic;">6 hours</span></p><p>Less than one year: <span style=" font-style:italic;">12 hours</span></p><p>More than one year: <span style=" font-style:italic;">1 day</span></p></body></html> - - - Use dynamic update timeouts - - - -- cgit v1.3.1 From 42ae919c9688ec45935983f62dcbb498ca77a205 Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 11 Feb 2019 22:51:58 -0600 Subject: Fix issue with downloads not being remove on failure Also adds helpful message when API key is not valid or present --- src/downloadmanager.cpp | 11 ++++++++++- src/nexusinterface.cpp | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 24942a06..df69d17a 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -1671,7 +1671,16 @@ void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, } } - removePending(gameName, modID, fileID); + QString gameRealName; + QStringList games(m_ManagedGame->validShortNames()); + games += m_ManagedGame->gameShortName(); + for (auto game : games) { + MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game); + if (gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { + gameRealName = gamePlugin->gameShortName(); + } + } + removePending(gameRealName, modID, fileID); emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString)); } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 265d898c..89b0bfde 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -539,6 +539,8 @@ void NexusInterface::nextRequest() return; } else if (getAccessManager()->validateWaiting()) { return; + } else { + qCritical("You must authorize MO2 in Settings -> Nexus to use the Nexus API."); } } -- cgit v1.3.1 From 8027fea6670e826a89f8738d3b592ca3d1fa76f0 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Thu, 21 Feb 2019 19:35:15 -0600 Subject: Grab the description for downloads instead of the change log --- src/downloadmanager.cpp | 4 ++-- src/nexusinterface.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index f1105581..41dd4dd0 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -1526,7 +1526,7 @@ void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVarian info->m_FileInfo->fileTime = QDateTime::fromMSecsSinceEpoch(fileInfo["uploaded_timestamp"].toLongLong()); info->m_FileInfo->fileID = fileInfo["file_id"].toInt(); info->m_FileInfo->fileName = fileInfo["file_name"].toString(); - info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["changelog_html"].toString()); + info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["description"].toString()); found = true; break; } @@ -1585,7 +1585,7 @@ void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int file info->fileName = result["file_name"].toString(); info->fileCategory = result["category_id"].toInt(); info->fileTime = QDateTime::fromMSecsSinceEpoch(result["uploaded_timestamp"].toLongLong()); - info->description = BBCode::convertToHTML(result["changelog_html"].toString()); + info->description = BBCode::convertToHTML(result["description"].toString()); info->repository = "Nexus"; diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 89b0bfde..0928f74a 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -94,7 +94,7 @@ void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userDa QVariantMap fileInfo = file.toMap(); temp.uri = fileInfo["file_name"].toString(); temp.name = fileInfo["name"].toString(); - temp.description = BBCode::convertToHTML(fileInfo["changelog_html"].toString()); + temp.description = BBCode::convertToHTML(fileInfo["description"].toString()); temp.version = VersionInfo(fileInfo["version"].toString()); temp.categoryID = fileInfo["category_id"].toInt(); temp.fileID = fileInfo["file_id"].toInt(); -- cgit v1.3.1 From 3a290b8a5aaf7dc913bf9628dcea488cf90c367d Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Thu, 21 Feb 2019 19:36:00 -0600 Subject: Fix warning about multiple consts --- src/nexusinterface.cpp | 2 +- src/nexusinterface.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 0928f74a..ee40be22 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -352,7 +352,7 @@ int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receive } int NexusInterface::requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, - const QString &subModule, const MOBase::IPluginGame const *game) + const QString &subModule, const MOBase::IPluginGame *game) { if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game); diff --git a/src/nexusinterface.h b/src/nexusinterface.h index ac9e466b..c18c7103 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -219,7 +219,7 @@ public: } int requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game); + const QString &subModule, const MOBase::IPluginGame *game); /** * @brief request nexus descriptions for multiple mods at once -- cgit v1.3.1 From ef80a8bacaab525be21b9beae4992955f5e907d0 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Fri, 22 Feb 2019 04:33:21 -0600 Subject: Remove the spoofed NMM version The new API does not require this and it serves no purpose to keep it. --- src/mainwindow.cpp | 3 +-- src/nexusinterface.cpp | 8 +------- src/nexusinterface.h | 7 ------- src/nxmaccessmanager.cpp | 6 ------ src/nxmaccessmanager.h | 3 --- src/organizercore.cpp | 1 - src/settings.cpp | 13 ------------- src/settings.h | 6 ------ src/settingsdialog.ui | 48 ------------------------------------------------ 9 files changed, 2 insertions(+), 93 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 282c9674..602c32df 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1878,6 +1878,7 @@ void MainWindow::processUpdates() { instance.remove("nexus_login"); instance.remove("nexus_api_key"); instance.remove("ask_for_nexuspw"); + instance.remove("nmm_version"); instance.endGroup(); instance.beginGroup("Servers"); instance.remove(""); @@ -4905,8 +4906,6 @@ void MainWindow::on_actionSettings_triggered() activateProxy(settings.useProxy()); } - NexusInterface::instance(&m_PluginContainer)->setNMMVersion(settings.getNMMVersion()); - updateDownloadView(); m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist()); diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index ee40be22..23dd7dbe 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -156,7 +156,7 @@ QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_NMMVersion(), m_PluginContainer(pluginContainer), m_RemainingDailyRequests(2500), m_RemainingHourlyRequests(100), m_MaxDailyRequests(2500), m_MaxHourlyRequests(100) + : m_PluginContainer(pluginContainer), m_RemainingDailyRequests(2500), m_RemainingHourlyRequests(100), m_MaxDailyRequests(2500), m_MaxHourlyRequests(100) { m_MOVersion = createVersionInfo(); @@ -187,12 +187,6 @@ void NexusInterface::setCacheDirectory(const QString &directory) m_AccessManager->setCache(m_DiskCache); } -void NexusInterface::setNMMVersion(const QString &nmmVersion) -{ - m_NMMVersion = nmmVersion; - m_AccessManager->setNMMVersion(nmmVersion); -} - void NexusInterface::loginCompleted() { nextRequest(); diff --git a/src/nexusinterface.h b/src/nexusinterface.h index c18c7103..c1f9da41 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -335,12 +335,6 @@ public: **/ void setCacheDirectory(const QString &directory); - /** - * MO has to send a "Nexus Client Vx.y.z" as part of the user agent to be allowed to use the API - * @param nmmVersion the version of nmm to impersonate - **/ - void setNMMVersion(const QString &nmmVersion); - /** * @brief called when the log-in completes. This was, requests waiting for the log-in can be run */ @@ -485,7 +479,6 @@ private: QQueue m_RequestQueue; MOBase::VersionInfo m_MOVersion; - QString m_NMMVersion; PluginContainer *m_PluginContainer; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index aca1d785..428aa3f3 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -69,11 +69,6 @@ NXMAccessManager::~NXMAccessManager() } } -void NXMAccessManager::setNMMVersion(const QString &nmmVersion) -{ - m_NMMVersion = nmmVersion; -} - QNetworkReply *NXMAccessManager::createRequest( QNetworkAccessManager::Operation operation, const QNetworkRequest &request, QIODevice *device) @@ -202,7 +197,6 @@ QString NXMAccessManager::userAgent(const QString &subModule) const else comments << QSysInfo::kernelType().left(1).toUpper() + QSysInfo::kernelType().mid(1) << QSysInfo::productType().left(1).toUpper() + QSysInfo::kernelType().mid(1) + " " + QSysInfo::productVersion(); - comments << "Nexus Client v" + m_NMMVersion; if (!subModule.isEmpty()) { comments << "module: " + subModule; } diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index 7fdb508f..da7736cc 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -41,8 +41,6 @@ public: ~NXMAccessManager(); - void setNMMVersion(const QString &nmmVersion); - bool validated() const; bool validateAttempted() const; @@ -101,7 +99,6 @@ private: QProgressDialog *m_ProgressDialog { nullptr }; QString m_MOVersion; - QString m_NMMVersion; QString m_ApiKey; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 2f75439d..caad45c9 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -292,7 +292,6 @@ OrganizerCore::OrganizerCore(const QSettings &initSettings) m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers()); NexusInterface::instance(m_PluginContainer)->setCacheDirectory(m_Settings.getCacheDirectory()); - NexusInterface::instance(m_PluginContainer)->setNMMVersion(m_Settings.getNMMVersion()); MOBase::QuestionBoxMemory::init(initSettings.fileName()); diff --git a/src/settings.cpp b/src/settings.cpp index 67a84c0a..942005f8 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -353,16 +353,6 @@ QString Settings::getOverwriteDirectory(bool resolve) const ToQString(AppConfig::overwritePath()), resolve); } -QString Settings::getNMMVersion() const -{ - static const QString MIN_NMM_VERSION = "0.65.2"; - QString result = m_Settings.value("Settings/nmm_version", MIN_NMM_VERSION).toString(); - if (VersionInfo(result) < VersionInfo(MIN_NMM_VERSION)) { - result = MIN_NMM_VERSION; - } - return result; -} - bool Settings::getNexusApiKey(QString &apiKey) const { QString tempKey = deObfuscate("APIKEY"); @@ -1179,7 +1169,6 @@ Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent, : Settings::SettingsTab(m_parent, m_dialog) , m_appIDEdit(m_dialog.findChild("appIDEdit")) , m_mechanismBox(m_dialog.findChild("mechanismBox")) - , m_nmmVersionEdit(m_dialog.findChild("nmmVersionEdit")) , m_hideUncheckedBox(m_dialog.findChild("hideUncheckedBox")) , m_forceEnableBox(m_dialog.findChild("forceEnableBox")) , m_displayForeignBox(m_dialog.findChild("displayForeignBox")) @@ -1214,7 +1203,6 @@ Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent, m_mechanismBox->setCurrentIndex(index); - m_nmmVersionEdit->setText(m_parent->getNMMVersion()); m_hideUncheckedBox->setChecked(m_parent->hideUncheckedPlugins()); m_forceEnableBox->setChecked(m_parent->forceEnableCoreFiles()); m_displayForeignBox->setChecked(m_parent->displayForeign()); @@ -1233,7 +1221,6 @@ void Settings::WorkaroundsTab::update() m_Settings.remove("Settings/app_id"); } m_Settings.setValue("Settings/load_mechanism", m_mechanismBox->itemData(m_mechanismBox->currentIndex()).toInt()); - m_Settings.setValue("Settings/nmm_version", m_nmmVersionEdit->text()); m_Settings.setValue("Settings/hide_unchecked_plugins", m_hideUncheckedBox->isChecked()); m_Settings.setValue("Settings/force_enable_core_files", m_forceEnableBox->isChecked()); m_Settings.setValue("Settings/display_foreign", m_displayForeignBox->isChecked()); diff --git a/src/settings.h b/src/settings.h index 2cd3f0b6..3744a493 100644 --- a/src/settings.h +++ b/src/settings.h @@ -152,11 +152,6 @@ public: **/ QString getModDirectory(bool resolve = true) const; - /** - * returns the version of nmm to impersonate when connecting to nexus - **/ - QString getNMMVersion() const; - /** * retrieve the directory where the web cache is stored (with native separators) **/ @@ -525,7 +520,6 @@ private: private: QLineEdit *m_appIDEdit; QComboBox *m_mechanismBox; - QLineEdit *m_nmmVersionEdit; QCheckBox *m_hideUncheckedBox; QCheckBox *m_forceEnableBox; QCheckBox *m_displayForeignBox; diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 127c94c7..e56b3435 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -959,53 +959,6 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - - - - - - NMM Version - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - The Version of Nexus Mod Manager to impersonate. - - - Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in. -On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn't need an update. Therefore you can configure the version to identify as here. -Please note that MO does identify itself as MO to the webserver, it's not lying about what it is. It is merely adding a "compatible" NMM version to the user agent. - -tl;dr-version: If Nexus-features don't work, insert the current version number of NMM here and try again. - - - 009.009.009 - - - Qt::AlignCenter - - - - - - - - @@ -1388,7 +1341,6 @@ programs you are intentionally running. pluginBlacklist appIDEdit mechanismBox - nmmVersionEdit bsaDateBtn tabWidget -- cgit v1.3.1 From d5332c5080ea5e1d0812cc900bb5ed6e20ff8f0e Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Wed, 6 Mar 2019 20:12:09 -0600 Subject: Add support for displaying tracked mods and setting tracked status --- src/mainwindow.cpp | 112 ++++ src/mainwindow.h | 4 + src/modflagicondelegate.cpp | 33 +- src/modinfo.h | 28 +- src/modinfoforeign.h | 2 + src/modinfooverwrite.h | 2 + src/modinforegular.cpp | 51 +- src/modinforegular.h | 22 + src/modlist.cpp | 1 + src/nexusinterface.cpp | 100 +++- src/nexusinterface.h | 46 +- src/organizer_en.ts | 1251 +++++++++++++++++++++---------------------- src/resources.qrc | 1 + src/resources/tracked.png | Bin 0 -> 1969 bytes 14 files changed, 1002 insertions(+), 651 deletions(-) create mode 100644 src/resources/tracked.png (limited to 'src/nexusinterface.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 58c80b87..3f6da2a6 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2826,6 +2826,73 @@ void MainWindow::unendorse_clicked() } } + +void MainWindow::trackMod(ModInfo::Ptr mod, bool doTrack) +{ + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + ModInfo::getByIndex(m_ContextRow)->track(doTrack); + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + m_OrganizerCore.doAfterLogin([&]() { this->trackMod(mod, doTrack); }); + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to track"), this); + } + } +} + + +void MainWindow::track_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + for (auto idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(true); + } + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + for (auto idx : selection->selectedRows()) { + auto modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + m_OrganizerCore.doAfterLogin([&]() { this->trackMod(modInfo, true); }); + } + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to track"), this); + } + } + } else { + trackMod(ModInfo::getByIndex(m_ContextRow), true); + } +} + +void MainWindow::untrack_clicked() +{ + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + for (auto idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(false); + } + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + for (auto idx : selection->selectedRows()) { + auto modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + m_OrganizerCore.doAfterLogin([&]() { this->trackMod(modInfo, false); }); + } + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to track"), this); + } + } + } else { + trackMod(ModInfo::getByIndex(m_ContextRow), false); + } +} + void MainWindow::validationFailed(const QString &error) { qDebug("Nexus API validation failed: %s", qUtf8Printable(error)); @@ -4009,6 +4076,7 @@ void MainWindow::checkModsForUpdates() if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { ModInfo::checkAllForUpdate(&m_PluginContainer, this); NexusInterface::instance(&m_PluginContainer)->requestEndorsementInfo(this, QVariant(), QString()); + NexusInterface::instance(&m_PluginContainer)->requestTrackingInfo(this, QVariant(), QString()); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { @@ -4595,6 +4663,22 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) } } + if (info->getNexusID() > 0) { + switch (info->trackedState()) { + case ModInfo::TRACKED_FALSE: { + menu.addAction(tr("Start tracking"), this, SLOT(track_clicked())); + } break; + case ModInfo::TRACKED_TRUE: { + menu.addAction(tr("Stop tracking"), this, SLOT(untrack_clicked())); + } break; + default: { + QAction *action = new QAction(tr("Tracked state unknown"), &menu); + action->setEnabled(false); + menu.addAction(action); + } break; + } + } + menu.addSeparator(); std::vector flags = info->getFlags(); @@ -5741,6 +5825,34 @@ void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultDa } } +void MainWindow::nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int) +{ + QMap gameNames; + for (auto game : m_PluginContainer.plugins()) { + gameNames[game->gameNexusName()] = game->gameShortName(); + } + + for (unsigned int i = 0; i < ModInfo::getNumMods(); i++) { + auto modInfo = ModInfo::getByIndex(i); + if (modInfo->getNexusID() <= 0) + continue; + + bool found = false; + auto resultsList = resultData.toList(); + for (auto item : resultsList) { + auto results = item.toMap(); + if ((gameNames[results["domain_name"].toString()].compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0) && + (results["mod_id"].toInt() == modInfo->getNexusID())) { + found = true; + break; + } + } + + modInfo->setIsTracked(found); + modInfo->saveMeta(); + } +} + void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultData, int) { QVariantList serverList = resultData.toList(); diff --git a/src/mainwindow.h b/src/mainwindow.h index ff23b8fb..152591e8 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -428,6 +428,8 @@ private slots: void endorse_clicked(); void dontendorse_clicked(); void unendorse_clicked(); + void track_clicked(); + void untrack_clicked(); void ignoreMissingData_clicked(); void markConverted_clicked(); void visitOnNexus_clicked(); @@ -517,6 +519,7 @@ private slots: void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); + void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int); void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); @@ -539,6 +542,7 @@ private slots: void resumeDownload(int downloadIndex); void endorseMod(ModInfo::Ptr mod); void unendorseMod(ModInfo::Ptr mod); + void trackMod(ModInfo::Ptr mod, bool doTrack); void cancelModListEditor(); void lockESPIndex(); diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp index 343c6ee3..fbd951eb 100644 --- a/src/modflagicondelegate.cpp +++ b/src/modflagicondelegate.cpp @@ -93,26 +93,27 @@ QList ModFlagIconDelegate::getIcons(const QModelIndex &index) const { QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) const { switch (flag) { - case ModInfo::FLAG_BACKUP: return ":/MO/gui/emblem_backup"; - case ModInfo::FLAG_INVALID: return ":/MO/gui/problem"; - case ModInfo::FLAG_NOTENDORSED: return ":/MO/gui/emblem_notendorsed"; - case ModInfo::FLAG_NOTES: return ":/MO/gui/emblem_notes"; - case ModInfo::FLAG_CONFLICT_MIXED: return ":/MO/gui/emblem_conflict_mixed"; - case ModInfo::FLAG_CONFLICT_OVERWRITE: return ":/MO/gui/emblem_conflict_overwrite"; - case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return ":/MO/gui/emblem_conflict_overwritten"; - case ModInfo::FLAG_CONFLICT_REDUNDANT: return ":MO/gui/emblem_conflict_redundant"; - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return ":/MO/gui/archive_loose_conflict_overwrite"; - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return ":/MO/gui/archive_loose_conflict_overwritten"; - case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return ":/MO/gui/archive_conflict_mixed"; - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return ":/MO/gui/archive_conflict_winner"; - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return ":/MO/gui/archive_conflict_loser"; - case ModInfo::FLAG_ALTERNATE_GAME: return ":MO/gui/alternate_game"; + case ModInfo::FLAG_BACKUP: return QStringLiteral(":/MO/gui/emblem_backup"); + case ModInfo::FLAG_INVALID: return QStringLiteral(":/MO/gui/problem"); + case ModInfo::FLAG_NOTENDORSED: return QStringLiteral(":/MO/gui/emblem_notendorsed"); + case ModInfo::FLAG_NOTES: return QStringLiteral(":/MO/gui/emblem_notes"); + case ModInfo::FLAG_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/emblem_conflict_mixed"); + case ModInfo::FLAG_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/emblem_conflict_overwrite"); + case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/emblem_conflict_overwritten"); + case ModInfo::FLAG_CONFLICT_REDUNDANT: return QStringLiteral(":/MO/gui/emblem_conflict_redundant"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwrite"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwritten"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/archive_conflict_mixed"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_conflict_winner"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_conflict_loser"); + case ModInfo::FLAG_ALTERNATE_GAME: return QStringLiteral(":/MO/gui/alternate_game"); case ModInfo::FLAG_FOREIGN: return QString(); case ModInfo::FLAG_SEPARATOR: return QString(); case ModInfo::FLAG_OVERWRITE: return QString(); case ModInfo::FLAG_PLUGIN_SELECTED: return QString(); - default: - qWarning("ModInfo flag %d has no defined icon", flag); + case ModInfo::FLAG_TRACKED: return QStringLiteral(":/MO/gui/tracked"); + default: + qWarning("ModInfo flag %d has no defined icon", flag); return QString(); } } diff --git a/src/modinfo.h b/src/modinfo.h index 52c29154..365dfe50 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -78,7 +78,8 @@ public: FLAG_ARCHIVE_CONFLICT_OVERWRITTEN, FLAG_ARCHIVE_CONFLICT_MIXED, FLAG_PLUGIN_SELECTED, - FLAG_ALTERNATE_GAME + FLAG_ALTERNATE_GAME, + FLAG_TRACKED, }; enum EContent { @@ -114,6 +115,12 @@ public: ENDORSED_NEVER }; + enum ETrackedState { + TRACKED_FALSE, + TRACKED_TRUE, + TRACKED_UNKNOWN, + }; + enum EModType { MOD_DEFAULT, MOD_DLC, @@ -362,6 +369,13 @@ public: */ virtual void setNeverEndorse() = 0; + /** + * update the tracked state for the mod. This only changes the + * buffered state, it does not sync with Nexus + * @param tracked the new tracked state + */ + virtual void setIsTracked(bool tracked) = 0; + /** * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices * @return true if the mod was successfully removed @@ -375,6 +389,13 @@ public: */ virtual void endorse(bool doEndorse) = 0; + /** + * @brief track or untrack the mod. This will sync with nexus! + * @param doTrack if true, the mod is tracked, if false, it's untracked. + * @note if doTrack doesn't differ from the current value, nothing happens. + */ + virtual void track(bool doTrack) = 0; + /** * @brief clear all caches held for this mod */ @@ -636,6 +657,11 @@ public: */ virtual EEndorsedState endorsedState() const { return ENDORSED_NEVER; } + /** + * @return true if the file is being tracked on nexus + */ + virtual ETrackedState trackedState() const { return TRACKED_FALSE; } + /** * @brief updates the valid-flag for this mod */ diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 1fdf5409..72fbb04f 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -29,8 +29,10 @@ public: virtual void addNexusCategory(int) {} virtual void setIsEndorsed(bool) {} virtual void setNeverEndorse() {} + virtual void setIsTracked(bool) {} virtual bool remove() { return false; } virtual void endorse(bool) {} + virtual void track(bool) {} virtual void parseNexusInfo() {} virtual bool isEmpty() const { return false; } virtual QString name() const; diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index e32653d5..ff0d8cd4 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -31,8 +31,10 @@ public: virtual void addNexusCategory(int) {} virtual void setIsEndorsed(bool) {} virtual void setNeverEndorse() {} + virtual void setIsTracked(bool) {} virtual bool remove() { return false; } virtual void endorse(bool) {} + virtual void track(bool) {} virtual void parseNexusInfo() {} virtual bool alwaysEnabled() const { return true; } virtual bool isEmpty() const; diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 2545b3f8..a271c4e8 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -35,6 +35,7 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa , m_Validated(false) , m_MetaInfoChanged(false) , m_EndorsedState(ENDORSED_UNKNOWN) + , m_TrackedState(TRACKED_UNKNOWN) , m_NexusBridge(pluginContainer) { testValid(); @@ -55,6 +56,8 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa , this, SLOT(nxmDescriptionAvailable(QString,int,QVariant,QVariant))); connect(&m_NexusBridge, SIGNAL(endorsementToggled(QString,int,QVariant,QVariant)) , this, SLOT(nxmEndorsementToggled(QString,int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(trackingToggled(QString,int,QVariant,bool)) + , this, SLOT(nxmTrackingToggled(QString,int,QVariant,bool))); connect(&m_NexusBridge, SIGNAL(requestFailed(QString,int,int,QVariant,QNetworkReply::NetworkError,QString)) , this, SLOT(nxmRequestFailed(QString,int,int,QVariant,QNetworkReply::NetworkError,QString))); } @@ -102,6 +105,7 @@ void ModInfoRegular::readMeta() m_LastNexusUpdate = QDateTime::fromString(metaFile.value("lastNexusUpdate", "").toString(), Qt::ISODate); m_NexusLastModified = QDateTime::fromString(metaFile.value("nexusLastModified", QDateTime::currentDateTimeUtc()).toString(), Qt::ISODate); m_Color = metaFile.value("color",QColor()).value(); + m_TrackedState = metaFile.value("tracked", false).toBool() ? TRACKED_TRUE : TRACKED_FALSE; if (metaFile.contains("endorsed")) { if (metaFile.value("endorsed").canConvert()) { switch (metaFile.value("endorsed").toInt()) { @@ -114,7 +118,7 @@ void ModInfoRegular::readMeta() m_EndorsedState = metaFile.value("endorsed", false).toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; } } - + QString categoriesString = metaFile.value("category", "").toString(); QStringList categories = categoriesString.split(',', QString::SkipEmptyParts); @@ -173,6 +177,7 @@ void ModInfoRegular::saveMeta() if (m_EndorsedState != ENDORSED_UNKNOWN) { metaFile.setValue("endorsed", m_EndorsedState); } + metaFile.setValue("tracked", m_TrackedState); metaFile.remove("installedFiles"); metaFile.beginWriteArray("installedFiles"); @@ -259,6 +264,18 @@ void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resu } +void ModInfoRegular::nxmTrackingToggled(QString, int, QVariant, bool tracked) +{ + if (tracked) + m_TrackedState = TRACKED_TRUE; + else + m_TrackedState = TRACKED_FALSE; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + + void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage) { QString fullMessage = errorMessage; @@ -414,6 +431,14 @@ void ModInfoRegular::setEndorsedState(EEndorsedState endorsedState) } } +void ModInfoRegular::setTrackedState(ETrackedState trackedState) +{ + if (trackedState != m_TrackedState) { + m_TrackedState = trackedState; + m_MetaInfoChanged = true; + } +} + void ModInfoRegular::setInstallationFile(const QString &fileName) { m_InstallationFile = fileName; @@ -433,13 +458,19 @@ void ModInfoRegular::setIsEndorsed(bool endorsed) } } - void ModInfoRegular::setNeverEndorse() { m_EndorsedState = ENDORSED_NEVER; m_MetaInfoChanged = true; } +void ModInfoRegular::setIsTracked(bool tracked) +{ + if (tracked != (m_TrackedState == TRACKED_TRUE)) { + m_TrackedState = tracked ? TRACKED_TRUE : TRACKED_FALSE; + m_MetaInfoChanged = true; + } +} void ModInfoRegular::setColor(QColor color) { @@ -465,6 +496,13 @@ void ModInfoRegular::endorse(bool doEndorse) } } +void ModInfoRegular::track(bool doTrack) +{ + if (doTrack != (m_TrackedState == TRACKED_TRUE)) { + m_NexusBridge.requestToggleTracking(m_GameName, getNexusID(), doTrack, QVariant(1)); + } +} + void ModInfoRegular::markConverted(bool converted) { m_Converted = converted; @@ -518,6 +556,10 @@ std::vector ModInfoRegular::getFlags() const Settings::instance().endorsementIntegration()) { result.push_back(ModInfo::FLAG_NOTENDORSED); } + if ((m_NexusID > 0) && + (trackedState() == TRACKED_TRUE)) { + result.push_back(ModInfo::FLAG_TRACKED); + } if (!isValid() && !m_Validated) { result.push_back(ModInfo::FLAG_INVALID); } @@ -667,6 +709,11 @@ ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const return m_EndorsedState; } +ModInfoRegular::ETrackedState ModInfoRegular::trackedState() const +{ + return m_TrackedState; +} + QDateTime ModInfoRegular::getLastNexusUpdate() const { return m_LastNexusUpdate; diff --git a/src/modinforegular.h b/src/modinforegular.h index aaab778a..f70487a2 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -177,6 +177,13 @@ public: */ virtual void setNeverEndorse(); + /** + * update the tracked state for the mod. This only changes the + * buffered state. It does not sync with Nexus + * @param tracked the new tracked state + */ + virtual void setIsTracked(bool tracked); + /** * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices * @return true if the mod was successfully removed @@ -190,6 +197,13 @@ public: */ virtual void endorse(bool doEndorse); + /** + * @brief track or untrack the mod. This will sync with nexus! + * @param doTrack if true, the mod is tracked, if false, it's untracked. + * @note if doTrack doesn't differ from the current value, nothing happens. + */ + virtual void track(bool doTrack); + /** * @brief updates the mod to flag it as converted in order to ignore the alternate game warning */ @@ -333,6 +347,11 @@ public: */ virtual EEndorsedState endorsedState() const; + /** + * @return true if the file is being tracked on nexus + */ + virtual ETrackedState trackedState() const; + /** * @brief get the last time nexus was checked for file updates on this mod */ @@ -391,11 +410,13 @@ public: private: void setEndorsedState(EEndorsedState endorsedState); + void setTrackedState(ETrackedState trackedState); private slots: void nxmDescriptionAvailable(QString, int modID, QVariant userData, QVariant resultData); void nxmEndorsementToggled(QString, int, QVariant userData, QVariant resultData); + void nxmTrackingToggled(QString, int, QVariant userData, bool tracked); void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage); protected: @@ -436,6 +457,7 @@ private: MOBase::VersionInfo m_IgnoredVersion; EEndorsedState m_EndorsedState; + ETrackedState m_TrackedState; NexusBridge m_NexusBridge; diff --git a/src/modlist.cpp b/src/modlist.cpp index 8e8ab2d2..3ca53949 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -169,6 +169,7 @@ QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten"); case ModInfo::FLAG_ALTERNATE_GAME: return tr("
    This mod is for a different game, " "make sure it's compatible or it could cause crashes."); + case ModInfo::FLAG_TRACKED: return tr("Mod is being tracked on the website"); default: return ""; } } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 23dd7dbe..b1f50fd8 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -29,6 +29,7 @@ along with Mod Organizer. If not, see . #include #include #include +#include #include @@ -68,6 +69,11 @@ void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule)); } +void NexusBridge::requestToggleTracking(QString gameName, int modID, bool track, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestToggleTracking(gameName, modID, track, this, userData, m_SubModule)); +} + void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { std::set::iterator iter = m_RequestIDs.find(requestID); @@ -142,6 +148,24 @@ void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant us } } +void NexusBridge::nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit trackedModsAvailable(userData, resultData); + } +} + +void NexusBridge::nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit trackingToggled(gameName, modID, userData, tracked); + } +} + void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage) { std::set::iterator iter = m_RequestIDs.find(requestID); @@ -472,7 +496,6 @@ int NexusInterface::requestEndorsementInfo(QObject *receiver, QVariant userData, return requestInfo.m_ID; } - int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { @@ -495,6 +518,43 @@ int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QStrin return -1; } +int NexusInterface::requestTrackingInfo(QObject *receiver, QVariant userData, const QString &subModule) +{ + NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_TRACKEDMODS, userData, subModule); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmTrackedModsAvailable(QVariant, QVariant, int)), + receiver, SLOT(nxmTrackedModsAvailable(QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + +int NexusInterface::requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLETRACKING, userData, subModule, game); + requestInfo.m_Track = track; + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmTrackingToggled(QString, int, QVariant, bool, int)), + receiver, SLOT(nxmTrackingToggled(QString, int, QVariant, bool, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; +} + IPluginGame* NexusInterface::getGame(QString gameName) const { auto gamePlugins = m_PluginContainer->plugins(); @@ -555,6 +615,7 @@ void NexusInterface::nextRequest() QJsonObject postObject; QJsonDocument postData(postObject); + bool requestIsDelete = false; QString url; if (!info.m_Reroute) { @@ -603,6 +664,15 @@ void NexusInterface::nextRequest() postObject.insert("Version", info.m_ModVersion); postData.setObject(postObject); } break; + case NXMRequestInfo::TYPE_TOGGLETRACKING: { + url = QStringLiteral("%1/user/tracked_mods?domain_name=%2").arg(info.m_URL).arg(info.m_GameName); + postObject.insert("mod_id", info.m_ModID); + postData.setObject(postObject); + requestIsDelete = !info.m_Track; + } break; + case NXMRequestInfo::TYPE_TRACKEDMODS: { + url = QStringLiteral("%1/user/tracked_mods").arg(info.m_URL); + } break; } } else { url = info.m_URL; @@ -617,10 +687,18 @@ void NexusInterface::nextRequest() request.setRawHeader("Application-Name", "MO2"); request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8()); - if (postData.object().isEmpty()) - info.m_Reply = m_AccessManager->get(request); - else + if (postData.object().isEmpty()) { + if (!requestIsDelete) { + info.m_Reply = m_AccessManager->get(request); + } else { + info.m_Reply = m_AccessManager->deleteResource(request); + } + } else if (!requestIsDelete) { info.m_Reply = m_AccessManager->post(request, postData.toJson()); + } else { + // Qt doesn't support DELETE with a payload as that's technically against the HTTP standard... + info.m_Reply = m_AccessManager->sendCustomRequest(request, "DELETE", postData.toJson()); + } connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); @@ -701,6 +779,20 @@ void NexusInterface::requestFinished(std::list::iterator iter) case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_TOGGLETRACKING: { + auto results = result.toMap(); + auto message = results["message"].toString(); + if (message.contains(QRegularExpression("User [0-9]+ is already Tracking Mod: [0-9]+")) || + message.contains(QRegularExpression("User [0-9]+ is now Tracking Mod: [0-9]+"))) { + emit nxmTrackingToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, true, iter->m_ID); + } else if (message.contains(QRegularExpression("User [0-9]+ is no longer tracking [0-9]+")) || + message.contains(QRegularExpression("Users is not tracking mod. Unable to untrack."))) { + emit nxmTrackingToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, false, iter->m_ID); + } + } break; + case NXMRequestInfo::TYPE_TRACKEDMODS: { + emit nxmTrackedModsAvailable(iter->m_UserData, result, iter->m_ID); + } break; } m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); diff --git a/src/nexusinterface.h b/src/nexusinterface.h index c1f9da41..a0f94562 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -98,6 +98,13 @@ public: */ virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData); + /** + * @brief requestToggleTracking + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + */ + virtual void requestToggleTracking(QString gameName, int modID, bool track, QVariant userData); + public slots: void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); @@ -106,6 +113,8 @@ public slots: void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID); + void nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage); private: @@ -330,6 +339,36 @@ public: int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + int requestTrackingInfo(QObject *receiver, QVariant userData, const QString &subModule); + + /** + * @param gameName the game short name to support multiple game sources + * @brief toggle tracking state of the mod + * @param modID id of the mod + * @param track true if the mod should be tracked, false for not tracked + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestToggleTracking(gameName, modID, track, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @param gameName the game short name to support multiple game sources + * @brief toggle tracking state of the mod + * @param modID id of the mod + * @param track true if the mod should be tracked, false for not tracked + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + /** * @param directory the directory to store cache files **/ @@ -402,6 +441,8 @@ signals: void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID); + void nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); void requestsChanged(int queueCount, std::tuple requestsRemaining); @@ -436,7 +477,9 @@ private: TYPE_ENDORSEMENTS, TYPE_TOGGLEENDORSEMENT, TYPE_GETUPDATES, - TYPE_CHECKUPDATES + TYPE_CHECKUPDATES, + TYPE_TOGGLETRACKING, + TYPE_TRACKEDMODS, } m_Type; UpdatePeriod m_UpdatePeriod; QVariant m_UserData; @@ -448,6 +491,7 @@ private: bool m_Reroute; int m_ID; int m_Endorse; + int m_Track; NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 7f8d3a9b..c8e7c286 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -829,58 +829,58 @@ File %3: %4 - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. - + failed to re-open %1 - + Unable to write download to drive (return %1). Check the drive's available storage. @@ -1497,7 +1497,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MainWindow - + Categories @@ -1563,20 +1563,20 @@ p, li { white-space: pre-wrap; } - + Restore Backup... - - + + Create Backup - + Active: @@ -1586,60 +1586,60 @@ p, li { white-space: pre-wrap; } - + List of available mods. - + This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders. - - - - + + + + Filter - + Nexus API Queued and Remaining Requests - + <html><head/><body><p>This tracks the number of queued Nexus API requests on the left (<span style=" font-weight:600;">Q</span>) and the remaining daily (<span style=" font-weight:600;">D</span>) and hourly (<span style=" font-weight:600;">H</span>) requests on the right. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens.</p></body></html> - + API: Q: 0 | D: 0 | H: 0 - + Clear all Filters - + No groups - + Nexus IDs - + Pick a program to run. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1649,12 +1649,12 @@ p, li { white-space: pre-wrap; } - + Run program - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1663,17 +1663,17 @@ p, li { white-space: pre-wrap; } - + Run - + Create a shortcut in your start menu or on the desktop to the specified program - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1682,32 +1682,32 @@ p, li { white-space: pre-wrap; } - + Shortcut - + Plugins - + Sort - + This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter. - + List of available esp/esm files - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1716,27 +1716,27 @@ p, li { white-space: pre-wrap; } - + Archives - + <html><head/><body><p>BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. <p>Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).</p><p>If there is a matching plugin, the game will load them no matter what.</p></body></html> - + <html><head/><body><p>Currently detected archives. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">What is an archive?</span></a>)</p></body></html> - + List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order. - + BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded. By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored! @@ -1744,72 +1744,72 @@ p, li { white-space: pre-wrap; } - + Data - + refresh data-directory overview - + Refresh the overview. This may take a moment. - - - - + + + + Refresh - + This is an overview of your data directory as visible to the game (and tools). - + File - + Mod - - + + Filters the above list so that only conflicts are displayed. - + Show only conflicts - - + + Filters the above list so that files from archives are not shown - + Show files from Archives - + Saves - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1820,197 +1820,197 @@ p, li { white-space: pre-wrap; } - + Downloads - + Refresh downloads view - + This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here. - + Show Hidden - + Tool Bar - + Install Mod - + Install &Mod - + Install a new mod from an archive - + Ctrl+M - + Profiles - + &Profiles - + Configure Profiles - + Ctrl+P - + Executables - + &Executables - + Configure the executables that can be started through Mod Organizer - + Ctrl+E - - + + Tools - + &Tools - + Ctrl+I - + Settings - + &Settings - + Configure settings and workarounds - + Ctrl+S - + Nexus - + Search nexus network for more mods - + Ctrl+N - - + + Update - + Mod Organizer is up-to-date - - + + No Notifications - + This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them. - - + + Help - + Ctrl+H - + Endorse MO - - + + Endorse Mod Organizer - + Copy Log to Clipboard - + Change Game - + Open the Instance selection dialog to manage a different Game @@ -2035,860 +2035,860 @@ p, li { white-space: pre-wrap; } - + Crash on exit - + MO crashed while exiting. Some settings may not be saved. Error: %1 - + Notifications - + There are notifications to read - + There are no notifications - - - + + + Endorse - + Won't Endorse - + Help on UI - + Documentation - + Chat on Discord - + Report Issue - + Tutorials - + About - + About Qt - + Name - + Please enter a name for the new profile - + failed to create profile: %1 - + Show tutorial? - + You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the "Help"-menu. - + Downloads in progress - + There are still downloads in progress, do you really want to quit? - + Plugin "%1" failed: %2 - + Plugin "%1" failed - + Browse Mod Page - + Also in: <br> - + No conflict - + <Edit...> - + This bsa is enabled in the ini file so it may be required! - + Activating Network Proxy - + Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues. - + Choose Mod - + Mod Archive - + Start Tutorial? - + You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue? - + failed to spawn notepad.exe: %1 - + failed to change origin name: %1 - + failed to move "%1" from mod "%2" to "%3": %4 - + <Contains %1> - + <Checked> - + <Unchecked> - + <Update> - + <Mod Backup> - + <Managed by MO> - + <Managed outside MO> - + <No category> - + <Conflicted> - + <Not Endorsed> - + failed to rename mod: %1 - + Overwrite? - + This will replace the existing mod "%1". Continue? - + failed to remove mod "%1" - - - + + + failed to rename "%1" to "%2" - - - - + + + + Confirm - + Remove the following mods?<br><ul>%1</ul> - + failed to remove mod: %1 - - - + + + Failed - + Installation file no longer exists - + Mods installed with old versions of MO can't be reinstalled in this way. - + Failed to create backup. - + You need to be logged in with Nexus to resume a download - - - - + + + + You need to be logged in with Nexus to endorse - - + + Endorsing multiple mods will take a while. Please wait... - - + + Unendorsing multiple mods will take a while. Please wait... - + Failed to display overwrite dialog: %1 - + Opening Nexus Links - + You are trying to open %1 links to Nexus Mods. Are you sure you want to do this? - + Nexus ID for this Mod is unknown - + Opening Web Pages - + You are trying to open %1 Web Pages. Are you sure you want to do this? - + Web page for this mod is unknown - + <table cellspacing="5"><tr><th>Type</th><th>All</th><th>Visible</th><tr><td>Enabled mods:&emsp;</td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr><tr><td>Unmanaged/DLCs:&emsp;</td><td align=right>%5</td><td align=right>%6</td></tr><tr><td>Mod backups:&emsp;</td><td align=right>%7</td><td align=right>%8</td></tr><tr><td>Separators:&emsp;</td><td align=right>%9</td><td align=right>%10</td></tr></table> - + <table cellspacing="6"><tr><th>Type</th><th>Active </th><th>Total</th></tr><tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr><tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr><tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr><tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr></table> - - - + + + Create Mod... - + This will create an empty mod. Please enter a name: - - + + A mod with this name already exists - + Create Separator... - + This will create a new separator. Please enter a name: - + A separator with this name already exists - + This will move all files from overwrite into a new, regular mod. Please enter a name: - + Move successful. - - + + Are you sure? - + About to recursively delete: - + Continue? - + The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated. - - + + Sorry - + I don't know a versioning scheme where %1 is newer than %2. - + Really enable all visible mods? - + Really disable all visible mods? - + Export to csv - + CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead. - + Select what mods you want export: - + All installed mods - + Only active (checked) mods from your current profile - + All currently visible mods in the mod list - + Choose what Columns to export: - + Mod_Priority - + Mod_Name - + Notes_column - + Mod_Status - + Primary_Category - + Nexus_ID - + Mod_Nexus_URL - + Mod_Version - + Install_Date - + Download_File_Name - + export failed: %1 - + Open Game folder - + Open MyGames folder - + Open INIs folder - + Open Instance folder - + Open Mods folder - + Open Profile folder - + Open Downloads folder - + Open MO2 Install folder - + Open MO2 Plugins folder - + Open MO2 Logs folder - + Install Mod... - + Create empty mod - + Create Separator - + Enable all visible - + Disable all visible - + Check for updates - + Export to csv... - - + + Send to - - + + Top - - + + Bottom - - + + Priority... - + Separator... - + All Mods - + Sync to Mods... - + Move content to Mod... - + Clear Overwrite... - - + + Open in Explorer - + Restore Backup - + Remove Backup... - - + + Change Categories - - + + Primary Category - + Rename Separator... - + Remove Separator... - + Select Color... - + Reset Color - + Change versioning scheme - + Force-check updates - + Un-ignore update - + Ignore update - - + + Enable selected - - + + Disable selected - + Rename Mod... - + Reinstall Mod - + Remove Mod... - + Un-Endorse - + Won't endorse - + Endorsement state unknown - + Ignore missing data - + Mark as converted/working - + Visit on Nexus - + Visit web page - + Information... - - + + Exception: - - + + Unknown exception - + <All> - + <Multiple> - + %1 more - + Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin. @@ -2896,12 +2896,12 @@ You can also use online editors and converters instead. - + Enable Mods... - + Delete %n save(s) @@ -2909,22 +2909,22 @@ You can also use online editors and converters instead. - + failed to remove %1 - + failed to create %1 - + Restarting MO - + Changing the managed game directory requires restarting MO. Any pending downloads will be paused. @@ -2932,373 +2932,373 @@ Click OK to restart MO now. - + Can't change download directory while downloads are in progress! - + failed to write to file %1 - + %1 written - + Select binary - + Binary - + Enter Name - + Please enter a name for the executable - + Not an executable - + This is not a recognized executable. - - + + Replace file? - + There already is a hidden version of this file. Replace it? - - + + File operation failed - - + + Failed to remove "%1". Maybe you lack the required file permissions? - + There already is a visible version of this file. Replace it? - - + + Set Priority - + Set the priority of the selected plugins - + file not found: %1 - + failed to generate preview for %1 - + Sorry, can't preview anything. This function currently does not support extracting from bsas. - + Update available - + Open/Execute - + Add as Executable - + Preview - + Un-Hide - + Hide - + Write To File... - + Do you want to endorse Mod Organizer on %1 now? - + Abstain from Endorsing Mod Organizer - + Are you sure you want to abstain from endorsing Mod Organizer 2? You will have to visit the mod page on the %1 Nexus site to change your mind. - - + + Thank you for endorsing MO2! :) - - + + Please reconsider endorsing MO2 on Nexus! - + Thank you! - + Thank you for your endorsement! - + Okay. - + This mod will not be endorsed and will no longer ask you to endorse. - + Mod ID %1 no longer seems to be available on Nexus. - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - - + + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + This will restart MO, continue? - + Edit Categories... - + Deselect filter - + Remove - + Enable all - + Disable all - + Unlock load order - + Lock load order - + Open Origin in Explorer - + Open Origin Info... - + depends on missing "%1" - + incompatible with "%1" - + Please wait while LOOT is running - + loot failed. Exit code was: %1 - + failed to start loot - + failed to run loot: %1 - + Errors occured - + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of modlist created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file - + Set the priority of the selected mods @@ -4043,18 +4043,18 @@ p, li { white-space: pre-wrap; } ModInfoRegular - + failed to write %1/meta.ini: error %2 - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> @@ -4205,178 +4205,183 @@ p, li { white-space: pre-wrap; } - + + Mod is being tracked on the website + + + + Non-MO - + invalid - + installed version: "%1", newest version: "%2" - + The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade". - + This file has been marked as "Old". There is most likely an updated version of this file available. - + This file has been marked as "Deleted"! You may want to check for an update or remove the nexus ID from this mod! - + %1 minute(s) and %2 second(s) - + This mod will be available to check in %2. - + Categories: <br> - + Invalid name - + Name is already in use by another mod - + drag&drop failed: %1 - + Confirm - + Are you sure you want to remove "%1"? - + Flags - + Content - + Mod Name - + Version - + Priority - + Category - + Source Game - + Nexus ID - + Installation - + Notes - - + + unknown - + Name of your mods - + Version of the mod (if available) - + Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority. - + Category of the mod. - + The source game which was the origin of this mod. - + Id of the mod as used on Nexus. - + Emblemes to highlight things that might require attention. - + Depicts the content of the mod:<br><table cellspacing=7><tr><td><img src=":/MO/gui/content/plugin" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr><tr><td><img src=":/MO/gui/content/interface" width=32/></td><td>Interface</td></tr><tr><td><img src=":/MO/gui/content/mesh" width=32/></td><td>Meshes</td></tr><tr><td><img src=":/MO/gui/content/bsa" width=32/></td><td>BSA</td></tr><tr><td><img src=":/MO/gui/content/texture" width=32/></td><td>Textures</td></tr><tr><td><img src=":/MO/gui/content/sound" width=32/></td><td>Sounds</td></tr><tr><td><img src=":/MO/gui/content/music" width=32/></td><td>Music</td></tr><tr><td><img src=":/MO/gui/content/string" width=32/></td><td>Strings</td></tr><tr><td><img src=":/MO/gui/content/script" width=32/></td><td>Scripts (Papyrus)</td></tr><tr><td><img src=":/MO/gui/content/skse" width=32/></td><td>Script Extender plugins</td></tr><tr><td><img src=":/MO/gui/content/skyproc" width=32/></td><td>SkyProc Patcher</td></tr><tr><td><img src=":/MO/gui/content/menu" width=32/></td><td>Mod Configuration Menu</td></tr><tr><td><img src=":/MO/gui/content/inifile" width=32/></td><td>INI files</td></tr><tr><td><img src=":/MO/gui/content/modgroup" width=32/></td><td>ModGroup files</td></tr></table> - + Time this mod was installed - + User notes about the mod @@ -4418,37 +4423,37 @@ p, li { white-space: pre-wrap; } NXMAccessManager - + Validating Nexus Connection - + Verifying Nexus login - + There was a timeout during the request - + Unknown error - + Validation failed, please reauthenticate in the Settings -> Nexus tab: %1 - + Could not parse response. Invalid JSON. - + Unknown error. @@ -4464,17 +4469,17 @@ p, li { white-space: pre-wrap; } NexusInterface - + Failed to guess mod id for "%1", please pick the correct one - + empty response - + invalid response @@ -4482,234 +4487,234 @@ p, li { white-space: pre-wrap; } OrganizerCore - - + + Failed to write settings - + An error occured trying to update MO settings to %1: %2 - + File is write protected - + Invalid file format (probably a bug) - + Unknown error %1 - + An error occured trying to write back MO settings to %1: %2 - - + + Download started - + Download failed - - - - + + + + Installation cancelled - - + + Another installation is currently in progress. - - + + Installation successful - - + + Configure Mod - - + + This mod contains ini tweaks. Do you want to configure them now? - - + + mod not found: %1 - - + + The mod was not installed completely. - + Executable not found: %1 - + Start Steam? - + Steam is required to be running already to correctly start the game. Should MO try to start steam now? - + Steam: Access Denied - + MO was denied access to the Steam process. This normally indicates that Steam is being run as administrator while MO is not. This can cause issues launching the game. It is recommended to not run Steam as administrator unless absolutely neccessary. Restart MO as administrator? - + Error - + Windows Event Log Error - + The Windows Event Log service is disabled and/or not running. This prevents USVFS from running properly. Your mods may not be working in the executable that you are launching. Note that you may have to restart MO and/or your PC after the service is fixed. Continue launching %1? - + Blacklisted Executable - + The executable you are attempted to launch is blacklisted in the virtual file system. This will likely prevent the executable, and any executables that are launched by this one, from seeing any mods. This could extend to INI files, save games and any other virtualized files. Continue launching %1? - + No profile set - + Failed to refresh list of esps: %1 - + Multiple esps/esls activated, please check that they don't conflict. - + Download? - + A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue? - - + + failed to update mod list: %1 - - + + login successful - + Login failed - + Login failed, try again? - + login failed: %1. Download will not be associated with an account - + login failed: %1 - + login failed: %1. You need to log-in with Nexus to update MO. - + MO1 "Script Extender" load mechanism has left hook.dll in your game folder - - + + Description missing - + <a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn. - + failed to save load order: %1 - + The designated write target "%1" is not enabled. @@ -5367,7 +5372,7 @@ p, li { white-space: pre-wrap; } - + Error @@ -5689,7 +5694,7 @@ If the folder was still in use, restart MO and try again. - + Failed to create "%1". Your user account probably lacks permission. @@ -5711,59 +5716,59 @@ If the folder was still in use, restart MO and try again. - + Canceled finding game in "%1". - + No game identified in "%1". The directory is required to contain the game binary. - + Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!) - + failed to start shortcut: %1 - + failed to start application: %1 - - + + Mod Organizer - + An instance of Mod Organizer is already running - + Failed to set up instance - + Please use "Help" from the toolbar to get usage instructions to all elements - - + + <Manage...> - + failed to parse profile %1: %2 @@ -5814,12 +5819,12 @@ If the folder was still in use, restart MO and try again. - + Script Extender - + Proxy DLL @@ -5966,58 +5971,58 @@ You will be asked if you want to allow helper.exe to make changes to the system. - + New update available (%1) - + Do you want to install update? All your mods and setup will be left untouched. Select Show Details option to see the full change-log. - + Install - + Download failed - + Failed to find correct download, please try again later. - + Update - + Download in progress - + Download failed: %1 - + Failed to install update: %1 - + Failed to start %1: %2 - + Error @@ -6035,35 +6040,35 @@ Select Show Details option to see the full change-log. - - + + attempt to store setting for unknown plugin "%1" - - + + Error - + Failed to retrieve a Nexus API key! Please try again. A browser window should open asking you to authorize. Failed to retrieve a Nexus API key! Please try again.A browser window should open asking you to authorize. - + Failed to create "%1", you may not have the necessary permission. path remains unchanged. - + Restart Mod Organizer? - + In order to reset the window geometries, MO must be restarted. Restart it now? @@ -6391,92 +6396,103 @@ p, li { white-space: pre-wrap; } - + + + <html><head/><body><p>By default, a counter is displayed under the mod list. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.</p></body></html> + + + + + Hide API Request Counter + + + + Associate with "Download with manager" links - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + Username - + Password - + If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure. - + Plugins - + Author: - + Version: - + Description: - + Key - + Value - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -6492,17 +6508,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -6513,47 +6529,28 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - - NMM Version - - - - - The Version of Nexus Mod Manager to impersonate. - - - - - Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in. -On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn't need an update. Therefore you can configure the version to identify as here. -Please note that MO does identify itself as MO to the webserver, it's not lying about what it is. It is merely adding a "compatible" NMM version to the user agent. - -tl;dr-version: If Nexus-features don't work, insert the current version number of NMM here and try again. - - - - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -6561,66 +6558,66 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Display mods installed outside MO - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - - + + Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - + Lock GUI when running executable - + Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness. - + <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html> - + Enable parsing of Archives (Experimental Feature) - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -6629,48 +6626,48 @@ programs you are intentionally running. - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running. - + Configure Executables Blacklist - - + + Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations. - + Reset Window Geometries - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - + Max Dumps To Keep - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -6678,12 +6675,12 @@ programs you are intentionally running. - + Hint: right click link and copy link location - + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -6693,17 +6690,17 @@ programs you are intentionally running. - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -6714,37 +6711,37 @@ programs you are intentionally running. - + None - + Mini (recommended) - + Data - + Full - + Log Level - + Decides the amount of data printed to "ModOrganizer.log" - + Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. @@ -6752,22 +6749,22 @@ programs you are intentionally running. - + Debug - + Info (recommended) - + Warning - + Error @@ -6782,12 +6779,12 @@ programs you are intentionally running. - + Executables Blacklist - + Enter one executable per line to be blacklisted from the virtual file system. Mods and other virtualized files will not be visible to these executables and any executables launched by them. @@ -6798,47 +6795,47 @@ Example: - + Select base directory - + Select download directory - + Select mod directory - + Select cache directory - + Select profiles directory - + Select overwrite directory - + Select game executable - + Confirm? - + This will make all dialogs show up again where you checked the "Remember selection"-box. Continue? diff --git a/src/resources.qrc b/src/resources.qrc index 5ed1780e..8645b27e 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -82,6 +82,7 @@ resources/archive-conflict-winner.png resources/game-warning.png resources/game-warning-16.png + resources/tracked.png resources/contents/jigsaw-piece.png diff --git a/src/resources/tracked.png b/src/resources/tracked.png new file mode 100644 index 00000000..11b1e8c8 Binary files /dev/null and b/src/resources/tracked.png differ -- cgit v1.3.1 From 70bcd97bd23756a92507006e6202eee7af902cd3 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Thu, 7 Mar 2019 16:32:17 -0600 Subject: Use MD5 when querying info before bothering the user --- src/downloadlist.cpp | 5 +- src/downloadlistwidget.cpp | 9 +++- src/downloadlistwidget.h | 2 + src/downloadmanager.cpp | 114 ++++++++++++++++++++++++++++++++++++++++++++- src/downloadmanager.h | 37 +++++++++------ src/mainwindow.cpp | 1 + src/nexusinterface.cpp | 60 ++++++++++++++++++++++++ src/nexusinterface.h | 18 +++++++ 8 files changed, 225 insertions(+), 21 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp index 765ee646..dd72abbd 100644 --- a/src/downloadlist.cpp +++ b/src/downloadlist.cpp @@ -104,8 +104,9 @@ QVariant DownloadList::data(const QModelIndex &index, int role) const case DownloadManager::STATE_CANCELED: return tr("Canceled"); case DownloadManager::STATE_PAUSED: return tr("Paused"); case DownloadManager::STATE_ERROR: return tr("Error"); - case DownloadManager::STATE_FETCHINGMODINFO: return tr("Fetching Info 1"); - case DownloadManager::STATE_FETCHINGFILEINFO: return tr("Fetching Info 2"); + case DownloadManager::STATE_FETCHINGMODINFO: return tr("Fetching Info"); + case DownloadManager::STATE_FETCHINGFILEINFO: return tr("Fetching Info"); + case DownloadManager::STATE_FETCHINGMODINFO_MD5: return tr("Fetching Info"); case DownloadManager::STATE_READY: return tr("Downloaded"); case DownloadManager::STATE_INSTALLED: return tr("Installed"); case DownloadManager::STATE_UNINSTALLED: return tr("Uninstalled"); diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp index be9d30e2..f0c4da1a 100644 --- a/src/downloadlistwidget.cpp +++ b/src/downloadlistwidget.cpp @@ -199,8 +199,8 @@ void DownloadListWidget::onCustomContextMenu(const QPoint &point) if (state >= DownloadManager::STATE_READY) { menu.addAction(tr("Install"), this, SLOT(issueInstall())); - if (m_Manager->isInfoIncomplete(m_ContextRow)) - menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfo())); + if (m_Manager->isInfoIncomplete(m_ContextRow)) + menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5())); else menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus())); menu.addAction(tr("Open File"), this, SLOT(issueOpenFile())); @@ -252,6 +252,11 @@ void DownloadListWidget::issueQueryInfo() emit queryInfo(m_ContextRow); } +void DownloadListWidget::issueQueryInfoMd5() +{ + emit queryInfoMd5(m_ContextRow); +} + void DownloadListWidget::issueDelete() { if (QMessageBox::question(nullptr, tr("Delete Files?"), diff --git a/src/downloadlistwidget.h b/src/downloadlistwidget.h index 4776d259..ad07b0f1 100644 --- a/src/downloadlistwidget.h +++ b/src/downloadlistwidget.h @@ -78,6 +78,7 @@ public: signals: void installDownload(int index); void queryInfo(int index); + void queryInfoMd5(int index); void removeDownload(int index, bool deleteFile); void restoreDownload(int index); void cancelDownload(int index); @@ -109,6 +110,7 @@ private slots: void issueRemoveFromViewCompleted(); void issueRemoveFromViewUninstalled(); void issueQueryInfo(); + void issueQueryInfoMd5(); private: DownloadManager *m_Manager; diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index c3a6a3e7..3563ecdb 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -262,7 +262,8 @@ void DownloadManager::pauseAll() foreach (DownloadInfo *info, m_ActiveDownloads) { if ((info->m_State < STATE_CANCELED) || (info->m_State == STATE_FETCHINGFILEINFO) || - (info->m_State == STATE_FETCHINGMODINFO)) { + (info->m_State == STATE_FETCHINGMODINFO) || + (info->m_State == STATE_FETCHINGMODINFO_MD5)) { done = false; break; } @@ -819,7 +820,9 @@ void DownloadManager::pauseDownload(int index) } else { setState(info, STATE_PAUSED); } - } else if ((info->m_State == STATE_FETCHINGMODINFO) || (info->m_State == STATE_FETCHINGFILEINFO)) { + } else if ((info->m_State == STATE_FETCHINGMODINFO) || + (info->m_State == STATE_FETCHINGFILEINFO) || + (info->m_State == STATE_FETCHINGMODINFO_MD5)) { setState(info, STATE_READY); } } @@ -952,6 +955,46 @@ void DownloadManager::queryInfo(int index) setState(info, STATE_FETCHINGMODINFO); } +void DownloadManager::queryInfoMd5(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("query: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + if (info->m_FileInfo->repository != "Nexus") { + qWarning("re-querying file info is currently only possible with Nexus"); + return; + } + + if (info->m_State < DownloadManager::STATE_READY) { + // UI shouldn't allow this + return; + } + + info->m_GamesToQuery << m_ManagedGame->gameShortName(); + info->m_GamesToQuery << m_ManagedGame->validShortNames(); + + QFile downloadFile(info->m_FileName); + if (!downloadFile.exists()) { + downloadFile.setFileName(m_OrganizerCore->downloadsPath() + "\\" + info->m_FileName); + } + if (!downloadFile.exists()) { + qDebug("Can't find download file %s", info->m_FileName); + return; + } + if (!downloadFile.open(QIODevice::ReadOnly)) { + qDebug("Can't open download file %s", info->m_FileName); + return; + } + info->m_Hash = QCryptographicHash::hash(downloadFile.readAll(), QCryptographicHash::Md5); + downloadFile.close(); + + info->m_ReQueried = true; + setState(info, STATE_FETCHINGMODINFO_MD5); +} + void DownloadManager::visitOnNexus(int index) { if ((index < 0) || (index >= m_ActiveDownloads.size())) { @@ -1329,6 +1372,9 @@ void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadMana case STATE_FETCHINGFILEINFO: { m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); } break; + case STATE_FETCHINGMODINFO_MD5: { + m_RequestIDs.insert(m_NexusInterface->requestInfoFromMd5(info->m_GamesToQuery[0], info->m_Hash, this, info->m_DownloadID, QString())); + } break; case STATE_READY: { createMetaFile(info); emit downloadComplete(row); @@ -1682,6 +1728,51 @@ void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int } +void DownloadManager::nxmFileInfoFromMd5Available(QString gameName, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + auto resultlist = resultData.toList(); + auto results = resultlist[0].toMap(); + auto fileDetails = results["file_details"].toMap(); + auto modDetails = results["mod"].toMap(); + + DownloadInfo *info = downloadInfoByID(userData.toInt()); + + info->m_FileInfo->name = fileDetails["name"].toString(); + info->m_FileInfo->fileID = fileDetails["file_id"].toInt(); + info->m_FileInfo->description = fileDetails["description"].toString(); + info->m_FileInfo->version.parse(fileDetails["version"].toString()); + if (!info->m_FileInfo->version.isValid()) + info->m_FileInfo->version.parse(fileDetails["mod_version"].toString()); + info->m_FileInfo->fileCategory = fileDetails["category_id"].toInt(); + + info->m_FileInfo->modID = modDetails["mod_id"].toInt(); + info->m_FileInfo->modName = modDetails["name"].toString(); + info->m_FileInfo->categoryID = modDetails["category_id"].toInt(); + + QString gameShortName = gameName; + QStringList games(m_ManagedGame->validShortNames()); + games += m_ManagedGame->gameShortName(); + for (auto game : games) { + MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game); + if (gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { + gameShortName = gamePlugin->gameShortName(); + break; + } + } + + info->m_FileInfo->gameName = gameShortName; + + setState(info, STATE_READY); +} + + void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString) { std::set::iterator idIter = m_RequestIDs.find(requestID); @@ -1691,10 +1782,29 @@ void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, m_RequestIDs.erase(idIter); } + DownloadInfo *userDataInfo = downloadInfoByID(userData.toInt()); + int index = 0; for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { DownloadInfo *info = *iter; + if (info != userDataInfo) + continue; + + // MD5 searches continue until all possible games are done + if (info->m_State == STATE_FETCHINGMODINFO_MD5) { + if (info->m_GamesToQuery.count() >= 2) { + info->m_GamesToQuery.pop_front(); + setState(info, STATE_FETCHINGMODINFO_MD5); + break; + } else { + info->m_State = STATE_READY; + queryInfo(index); + emit update(index); + break; + } + } + if (info->m_FileInfo->modID == modID) { if (info->m_State < STATE_FETCHINGMODINFO) { m_ActiveDownloads.erase(iter); diff --git a/src/downloadmanager.h b/src/downloadmanager.h index 841a9fbc..8033989e 100644 --- a/src/downloadmanager.h +++ b/src/downloadmanager.h @@ -61,6 +61,7 @@ public: STATE_ERROR, STATE_FETCHINGMODINFO, STATE_FETCHINGFILEINFO, + STATE_FETCHINGMODINFO_MD5, STATE_NOFETCH, STATE_READY, STATE_INSTALLED, @@ -86,6 +87,8 @@ private: qint64 m_ResumePos; qint64 m_TotalSize; QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere + QByteArray m_Hash; + QStringList m_GamesToQuery; int m_Tries; bool m_ReQueried; @@ -153,16 +156,16 @@ public: **/ void setOutputDirectory(const QString &outputDirectory); - /** - * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called - * - **/ - static void startDisableDirWatcher(); + /** + * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called + * + **/ + static void startDisableDirWatcher(); - /** - * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called - **/ - static void endDisableDirWatcher(); + /** + * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called + **/ + static void endDisableDirWatcher(); /** * @return current download directory @@ -290,7 +293,7 @@ public: * the following states: * started -> downloading -> fetching mod info -> fetching file info -> done * in case of downloads started via nxm-link, file information is fetched first - * + * * @param index index of the file to look up * @return the download state **/ @@ -444,7 +447,9 @@ public slots: void queryInfo(int index); - void visitOnNexus(int index); + void queryInfoMd5(int index); + + void visitOnNexus(int index); void openFile(int index); @@ -458,6 +463,8 @@ public slots: void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmFileInfoFromMd5Available(QString gameName, QVariant userData, QVariant resultData, int requestID); + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); void managedGameChanged(MOBase::IPluginGame const *gamePlugin); @@ -547,10 +554,10 @@ private: QFileSystemWatcher m_DirWatcher; - //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files - //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder. - //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui. - static int m_DirWatcherDisabler; + //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files + //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder. + //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui. + static int m_DirWatcherDisabler; std::map m_DownloadFails; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3f6da2a6..0bdefe80 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5518,6 +5518,7 @@ void MainWindow::initDownloadView() connect(ui->downloadView, SIGNAL(installDownload(int)), &m_OrganizerCore, SLOT(installDownload(int))); connect(ui->downloadView, SIGNAL(queryInfo(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfo(int))); + connect(ui->downloadView, SIGNAL(queryInfoMd5(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfoMd5(int))); connect(ui->downloadView, SIGNAL(visitOnNexus(int)), m_OrganizerCore.downloadManager(), SLOT(visitOnNexus(int))); connect(ui->downloadView, SIGNAL(openFile(int)), m_OrganizerCore.downloadManager(), SLOT(openFile(int))); connect(ui->downloadView, SIGNAL(openInDownloadsFolder(int)), m_OrganizerCore.downloadManager(), SLOT(openInDownloadsFolder(int))); diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index b1f50fd8..33c5983c 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -555,6 +555,23 @@ int NexusInterface::requestToggleTracking(QString gameName, int modID, bool trac return -1; } +int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(hash, NXMRequestInfo::TYPE_FILEINFO_MD5, userData, subModule, game); + requestInfo.m_Hash = hash; + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), + receiver, SLOT(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + IPluginGame* NexusInterface::getGame(QString gameName) const { auto gamePlugins = m_PluginContainer->plugins(); @@ -673,6 +690,9 @@ void NexusInterface::nextRequest() case NXMRequestInfo::TYPE_TRACKEDMODS: { url = QStringLiteral("%1/user/tracked_mods").arg(info.m_URL); } break; + case NXMRequestInfo::TYPE_FILEINFO_MD5: { + url = QStringLiteral("%1/games/%2/mods/md5_search/%3").arg(info.m_URL).arg(info.m_GameName).arg(QString(info.m_Hash.toHex())); + } } } else { url = info.m_URL; @@ -719,6 +739,7 @@ void NexusInterface::requestFinished(std::list::iterator iter) if (reply->error() != QNetworkReply::NoError) { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 429) { if (reply->rawHeader("x-rl-daily-remaining").toInt() || reply->rawHeader("x-rl-hourly-remaining").toInt()) qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team."); @@ -793,6 +814,9 @@ void NexusInterface::requestFinished(std::list::iterator iter) case NXMRequestInfo::TYPE_TRACKEDMODS: { emit nxmTrackedModsAvailable(iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_FILEINFO_MD5: { + emit nxmFileInfoFromMd5Available(iter->m_GameName, iter->m_UserData, result, iter->m_ID); + } break; } m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); @@ -890,6 +914,8 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) , m_Endorse(false) + , m_Track(false) + , m_Hash(QByteArray()) { } @@ -915,6 +941,8 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) , m_Endorse(false) + , m_Track(false) + , m_Hash(QByteArray()) {} NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID @@ -939,6 +967,8 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) , m_Endorse(false) + , m_Track(false) + , m_Hash(QByteArray()) {} NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type @@ -960,6 +990,8 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type , m_NexusGameID(0) , m_GameName("") , m_Endorse(false) + , m_Track(false) + , m_Hash(QByteArray()) {} @@ -984,4 +1016,32 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) , m_Endorse(false) + , m_Track(false) + , m_Hash(QByteArray()) +{} + + +NexusInterface::NXMRequestInfo::NXMRequestInfo(QByteArray &hash + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(0) + , m_ModVersion("0") + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url()) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) + , m_Track(false) + , m_Hash(hash) {} diff --git a/src/nexusinterface.h b/src/nexusinterface.h index a0f94562..6f3c9cd1 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -369,6 +369,20 @@ public: int requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + /** + * + */ + int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestInfoFromMd5(gameName, hash, receiver, userData, subModule, getGame(gameName)); + } + + /** + * + */ + int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + /** * @param directory the directory to store cache files **/ @@ -438,6 +452,7 @@ signals: void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmFileInfoFromMd5Available(QString gameName, QVariant userData, QVariant resultData, int requestID); void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); @@ -480,6 +495,7 @@ private: TYPE_CHECKUPDATES, TYPE_TOGGLETRACKING, TYPE_TRACKEDMODS, + TYPE_FILEINFO_MD5, } m_Type; UpdatePeriod m_UpdatePeriod; QVariant m_UserData; @@ -492,12 +508,14 @@ private: int m_ID; int m_Endorse; int m_Track; + QByteArray m_Hash; NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(Type type, QVariant userData, const QString &subModule); NXMRequestInfo(UpdatePeriod period, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(QByteArray &hash, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); private: static QAtomicInt s_NextID; -- cgit v1.3.1 From d5ba8802830351e19bcf01e42090b81d5fa623d0 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Thu, 7 Mar 2019 16:55:42 -0600 Subject: Suppress expected network errors during MD5 searches --- src/downloadmanager.cpp | 5 +++-- src/nexusinterface.cpp | 12 +++++++++--- src/nexusinterface.h | 2 ++ 3 files changed, 14 insertions(+), 5 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 3563ecdb..25c7e04b 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -1373,6 +1373,7 @@ void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadMana m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); } break; case STATE_FETCHINGMODINFO_MD5: { + qDebug(qUtf8Printable(QString("Searching %1 for Md5 of %2").arg(info->m_GamesToQuery[0]).arg(QString(info->m_Hash.toHex())))); m_RequestIDs.insert(m_NexusInterface->requestInfoFromMd5(info->m_GamesToQuery[0], info->m_Hash, this, info->m_DownloadID, QString())); } break; case STATE_READY: { @@ -1796,12 +1797,12 @@ void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, if (info->m_GamesToQuery.count() >= 2) { info->m_GamesToQuery.pop_front(); setState(info, STATE_FETCHINGMODINFO_MD5); - break; + return; } else { info->m_State = STATE_READY; queryInfo(index); emit update(index); - break; + return; } } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 33c5983c..6194cd30 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -560,6 +560,8 @@ int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray &hash, QObje { NXMRequestInfo requestInfo(hash, NXMRequestInfo::TYPE_FILEINFO_MD5, userData, subModule, game); requestInfo.m_Hash = hash; + requestInfo.m_AllowedErrors[QNetworkReply::NetworkError::ContentNotFoundError].append(404); + requestInfo.m_IgnoreGenericErrorHandler = true; m_RequestQueue.enqueue(requestInfo); connect(this, SIGNAL(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), @@ -721,7 +723,8 @@ void NexusInterface::nextRequest() } connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); - connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + if (!info.m_IgnoreGenericErrorHandler) + connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); info.m_Timeout->start(); m_ActiveRequest.push_back(info); @@ -737,10 +740,13 @@ void NexusInterface::requestFinished(std::list::iterator iter) { QNetworkReply *reply = iter->m_Reply; - if (reply->error() != QNetworkReply::NoError) { + auto error = reply->error(); + if (error != QNetworkReply::NoError) { int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (statusCode == 429) { + if (iter->m_AllowedErrors.contains(error) && iter->m_AllowedErrors[error].contains(statusCode)) { + // These errors are allows to silently happen. They should be handled in nxmRequestFailed below. + } else if (statusCode == 429) { if (reply->rawHeader("x-rl-daily-remaining").toInt() || reply->rawHeader("x-rl-hourly-remaining").toInt()) qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team."); else diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 6f3c9cd1..ac7f61c5 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -509,6 +509,8 @@ private: int m_Endorse; int m_Track; QByteArray m_Hash; + QMap> m_AllowedErrors; + bool m_IgnoreGenericErrorHandler; NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); -- cgit v1.3.1 From b6be2863f2afe7a9d975692484924bd68d1052da Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Fri, 22 Mar 2019 01:10:01 -0500 Subject: Set the remaining API requests on a 429 reply --- src/nexusinterface.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 6194cd30..3989aa35 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -564,7 +564,7 @@ int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray &hash, QObje requestInfo.m_IgnoreGenericErrorHandler = true; m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), + connect(this, SIGNAL(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), receiver, SLOT(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), Qt::UniqueConnection); connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), @@ -747,10 +747,23 @@ void NexusInterface::requestFinished(std::list::iterator iter) if (iter->m_AllowedErrors.contains(error) && iter->m_AllowedErrors[error].contains(statusCode)) { // These errors are allows to silently happen. They should be handled in nxmRequestFailed below. } else if (statusCode == 429) { - if (reply->rawHeader("x-rl-daily-remaining").toInt() || reply->rawHeader("x-rl-hourly-remaining").toInt()) + m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); + m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt(); + m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt(); + m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt(); + + if (m_RemainingDailyRequests || m_RemainingHourlyRequests) qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team."); else qWarning("All API requests have been consumed and are now being denied."); + + emit requestsChanged(m_RequestQueue.size(), std::tuple(std::make_tuple( + m_RemainingDailyRequests, + m_MaxDailyRequests, + m_RemainingHourlyRequests, + m_MaxHourlyRequests + ))); + qWarning("Error: %s", reply->errorString().toUtf8().constData()); } else { qWarning("request failed: %s", reply->errorString().toUtf8().constData()); -- cgit v1.3.1 From eb9ac6ca246c13a4f7d6d60a9ec3e9018fe209e8 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Mon, 25 Mar 2019 04:42:02 -0500 Subject: Add more protection against missing game plugins --- src/downloadmanager.cpp | 6 +++--- src/mainwindow.cpp | 2 +- src/nexusinterface.cpp | 26 +++++++++++++++++++++++--- src/organizercore.cpp | 2 +- 4 files changed, 28 insertions(+), 8 deletions(-) (limited to 'src/nexusinterface.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 3fa282da..25542ef3 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -453,7 +453,7 @@ void DownloadManager::removePending(QString gameName, int modID, int fileID) games += m_ManagedGame->gameShortName(); for (auto game : games) { MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game); - if (gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { + if (gamePlugin != nullptr && gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { gameShortName = gamePlugin->gameShortName(); break; } @@ -1640,7 +1640,7 @@ void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int file games += m_ManagedGame->gameShortName(); for (auto game : games) { MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game); - if (gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { + if (gamePlugin != nullptr && gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { info->gameName = gamePlugin->gameShortName(); } } @@ -1815,7 +1815,7 @@ void DownloadManager::nxmFileInfoFromMd5Available(QString gameName, QVariant use games += m_ManagedGame->gameShortName(); for (auto game : games) { MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game); - if (gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { + if (gamePlugin != nullptr && gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) { gameShortName = gamePlugin->gameShortName(); break; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ea46b175..1569eba5 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5623,7 +5623,7 @@ void MainWindow::nxmEndorsementsAvailable(QVariant userData, QVariant resultData } for (auto game : games) { IPluginGame *gamePlugin = m_OrganizerCore.getGame(game); - if (gamePlugin->gameShortName().compare("SkyrimSE", Qt::CaseInsensitive) == 0) + if (gamePlugin != nullptr && gamePlugin->gameShortName().compare("SkyrimSE", Qt::CaseInsensitive) == 0) searchedMO2NexusGame = true; auto iter = sorted.equal_range(gamePlugin->gameNexusName()); for (auto result = iter.first; result != iter.second; ++result) { diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 3989aa35..9db0d54e 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -287,13 +287,23 @@ bool NexusInterface::isURLGameRelated(const QUrl &url) const QString NexusInterface::getGameURL(QString gameName) const { IPluginGame *game = getGame(gameName); - return "https://www.nexusmods.com/" + game->gameNexusName().toLower(); + if (game != nullptr) { + return "https://www.nexusmods.com/" + game->gameNexusName().toLower(); + } else { + qCritical("getGameURL can't find plugin for %s", qUtf8Printable(gameName)); + return ""; + } } QString NexusInterface::getOldModsURL(QString gameName) const { IPluginGame *game = getGame(gameName); - return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods"; + if (game != nullptr) { + return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods"; + } else { + qCritical("getOldModsURL can't find plugin for %s", qUtf8Printable(gameName)); + return ""; + } } @@ -395,6 +405,11 @@ int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant { if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { IPluginGame *game = getGame(gameName); + if (game == nullptr) { + qCritical("requestUpdates can't find plugin for %s", qUtf8Printable(gameName)); + return -1; + } + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); m_RequestQueue.enqueue(requestInfo); @@ -451,6 +466,11 @@ int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) { IPluginGame *gamePlugin = getGame(gameName); + if (gamePlugin == nullptr) { + qCritical("requestFileInfo can't find plugin for %s", qUtf8Printable(gameName)); + return -1; + } + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin); m_RequestQueue.enqueue(requestInfo); @@ -579,7 +599,7 @@ IPluginGame* NexusInterface::getGame(QString gameName) const auto gamePlugins = m_PluginContainer->plugins(); IPluginGame *gamePlugin = qApp->property("managed_game").value(); for (auto plugin : gamePlugins) { - if (plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + if (plugin != nullptr && plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { gamePlugin = plugin; break; } diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 687fd6c9..747cfaba 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -885,7 +885,7 @@ MOBase::IModInterface *OrganizerCore::getMod(const QString &name) const MOBase::IPluginGame *OrganizerCore::getGame(const QString &name) const { for (IPluginGame *game : m_PluginContainer->plugins()) { - if (game->gameShortName().compare(name, Qt::CaseInsensitive) == 0) + if (game != nullptr && game->gameShortName().compare(name, Qt::CaseInsensitive) == 0) return game; } return nullptr; -- cgit v1.3.1