diff options
| author | Silarn <jrim@rimpo.org> | 2018-08-20 18:34:31 -0500 |
|---|---|---|
| committer | Silarn <jrim@rimpo.org> | 2019-02-18 21:26:44 -0600 |
| commit | 9fe2f6126dc7b9396d188b57ccb097f0035f57b7 (patch) | |
| tree | dfb6038767f55238506122682b644ac86415050b /src/organizercore.cpp | |
| parent | bd9cc6254c8ded2f726e2b001a4ecf61ac0e5a1d (diff) | |
Initial Nexus API changes:
- Switch to SSO with WebSocket
- Update endpoints (all but version checking)
Diffstat (limited to 'src/organizercore.cpp')
| -rw-r--r-- | src/organizercore.cpp | 4946 |
1 files changed, 2461 insertions, 2485 deletions
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 <ipluginmodpage.h>
-#include <dataarchives.h>
-#include <localsavegames.h>
-#include <directoryentry.h>
-#include <scopeguard.h>
-#include <utility.h>
-#include <usvfs.h>
-#include "appconfig.h"
-#include <report.h>
-#include <questionboxmemory.h>
-#include "lockeddialog.h"
-#include "instancemanager.h"
-#include <scriptextender.h>
-
-#include <QApplication>
-#include <QCoreApplication>
-#include <QDialog>
-#include <QDialogButtonBox>
-#include <QMessageBox>
-#include <QNetworkInterface>
-#include <QProcess>
-#include <QTimer>
-#include <QUrl>
-#include <QWidget>
-
-#include <QtDebug>
-#include <QtGlobal> // for qUtf8Printable, etc
-
-#include <Psapi.h>
-#include <Shlobj.h>
-#include <tlhelp32.h>
-#include <tchar.h> // for _tcsicmp
-
-#include <limits.h>
-#include <stddef.h>
-#include <string.h> // for memset, wcsrchr
-
-#include <exception>
-#include <functional>
-#include <boost/algorithm/string/predicate.hpp>
-#include <memory>
-#include <set>
-#include <string> //for wstring
-#include <tuple>
-#include <utility>
-
-
-using namespace MOShared;
-using namespace MOBase;
-
-//static
-CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None;
-
-static bool isOnline()
-{
- QList<QNetworkInterface> 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 <typename InputIterator>
-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<Executable>::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<DWORD[]> processIDs;
- DWORD bytesReturned;
- bool success = false;
- while (!success) {
- processIDs.reset(new DWORD[currentSize]);
- if (!::EnumProcesses(processIDs.get(),
- static_cast<DWORD>(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<uint>)), this, SLOT(modStatusChanged(QList<uint>)));
- 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<IPluginGame>()) {
- if (game->gameShortName().compare(name, Qt::CaseInsensitive) == 0)
- return game;
- }
- return nullptr;
-}
-
-MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &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<QString> 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<QString> modName;
-
- // see if there already are mods with the specified mod id
- if (modID != 0) {
- std::vector<ModInfo::Ptr> modInfo = ModInfo::getByModID(gameName, modID);
- for (auto iter = modInfo.begin(); iter != modInfo.end(); ++iter) {
- std::vector<ModInfo::EFlag> 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<DirectoryEntry *>::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<bool(const QString &)> &filter) const
-{
- QStringList result;
- DirectoryEntry *dir = m_DirectoryStructure;
- if (!path.isEmpty())
- dir = dir->findSubDirectoryRecursive(ToWString(path));
- if (dir != nullptr) {
- std::vector<FileEntry::Ptr> 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<MOBase::IOrganizer::FileInfo> OrganizerCore::findFileInfos(
- const QString &path,
- const std::function<bool(const MOBase::IOrganizer::FileInfo &)> &filter)
- const
-{
- QList<IOrganizer::FileInfo> result;
- DirectoryEntry *dir = m_DirectoryStructure;
- if (!path.isEmpty())
- dir = dir->findSubDirectoryRecursive(ToWString(path));
- if (dir != nullptr) {
- std::vector<FileEntry::Ptr> 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<MOBase::ExecutableForcedLoadSetting> &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<MOBase::ExecutableForcedLoadSetting> &forcedLibraries,
- LPDWORD exitCode)
-{
- HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries);
- if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) {
- std::unique_ptr<LockedDialog> 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<MOBase::ExecutableForcedLoadSetting> &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<ExecutableForcedLoadSetting> 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<QString> 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<QString>& 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<bool(const QString &)> &func)
-{
- auto conn = m_AboutToRun.connect(func);
- return conn.connected();
-}
-
-bool OrganizerCore::onFinishedRun(
- const std::function<void(const QString &, unsigned int)> &func)
-{
- auto conn = m_FinishedRun.connect(func);
- return conn.connected();
-}
-
-bool OrganizerCore::onModInstalled(
- const std::function<void(const QString &)> &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<DataArchives>();
-
- 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<unsigned int> modsToUpdate;
- modsToUpdate.append(index);
- updateModsActiveState(modsToUpdate, active);
-}
-
-void OrganizerCore::updateModsActiveState(const QList<unsigned int> &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<unsigned int, ModInfo::Ptr> allModInfo;
- allModInfo[index] = modInfo;
- updateModsInDirectoryStructure(allModInfo);
-}
-
-void OrganizerCore::updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::Ptr> 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<QString> archives = enabledArchives();
- m_DirectoryRefresher.setMods(
- m_CurrentProfile->getActiveMods(),
- std::set<QString>(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<MOBase::IPluginModPage>()) {
- 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<QString> OrganizerCore::enabledArchives()
-{
- std::vector<QString> 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<std::tuple<QString, QString, int>> activeModList
- = m_CurrentProfile->getActiveMods();
- auto archives = enabledArchives();
- m_DirectoryRefresher.setMods(
- activeModList, std::set<QString>(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<unsigned int> index) {
- try {
- QMap<unsigned int, ModInfo::Ptr> modsToEnable;
- QMap<unsigned int, ModInfo::Ptr> 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<ModInfo::EFlag> 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<ScriptExtender>()) {
- QString hookdll = QDir::toNativeSeparators(
- managedGame()->dataDirectory().absoluteFilePath(extender->PluginPath() + "/hook.dll"));
- if (QFile(hookdll).exists())
- return hookdll;
- }
- return QString();
-}
-
-std::vector<unsigned int> OrganizerCore::activeProblems() const
-{
- std::vector<unsigned int> 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("<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.").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<Mapping> 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<IPluginGame *>();
- 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<LocalSavegames>();
- 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<MOBase::IPluginFileMapper>()) {
- IPlugin *plugin = dynamic_cast<IPlugin *>(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<Mapping> OrganizerCore::fileMapping(
- const QString &dataPath, const QString &relPath, const DirectoryEntry *base,
- const DirectoryEntry *directoryEntry, int createDestination)
-{
- std::vector<Mapping> 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<DirectoryEntry *>::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<Mapping> 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 <ipluginmodpage.h> +#include <dataarchives.h> +#include <localsavegames.h> +#include <directoryentry.h> +#include <scopeguard.h> +#include <utility.h> +#include <usvfs.h> +#include "appconfig.h" +#include <report.h> +#include <questionboxmemory.h> +#include "lockeddialog.h" +#include "instancemanager.h" +#include <scriptextender.h> + +#include <QApplication> +#include <QCoreApplication> +#include <QDialog> +#include <QDialogButtonBox> +#include <QMessageBox> +#include <QNetworkInterface> +#include <QProcess> +#include <QTimer> +#include <QUrl> +#include <QWidget> + +#include <QtDebug> +#include <QtGlobal> // for qUtf8Printable, etc + +#include <Psapi.h> +#include <Shlobj.h> +#include <tlhelp32.h> +#include <tchar.h> // for _tcsicmp + +#include <limits.h> +#include <stddef.h> +#include <string.h> // for memset, wcsrchr + +#include <exception> +#include <functional> +#include <boost/algorithm/string/predicate.hpp> +#include <memory> +#include <set> +#include <string> //for wstring +#include <tuple> +#include <utility> + + +using namespace MOShared; +using namespace MOBase; + +//static +CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None; + +static bool isOnline() +{ + QList<QNetworkInterface> 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 <typename InputIterator> +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<Executable>::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<DWORD[]> processIDs; + DWORD bytesReturned; + bool success = false; + while (!success) { + processIDs.reset(new DWORD[currentSize]); + if (!::EnumProcesses(processIDs.get(), + static_cast<DWORD>(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<uint>)), this, SLOT(modStatusChanged(QList<uint>))); + 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<IPluginGame>()) { + if (game->gameShortName().compare(name, Qt::CaseInsensitive) == 0) + return game; + } + return nullptr; +} + +MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &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<QString> 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<QString> modName; + + // see if there already are mods with the specified mod id + if (modID != 0) { + std::vector<ModInfo::Ptr> modInfo = ModInfo::getByModID(gameName, modID); + for (auto iter = modInfo.begin(); iter != modInfo.end(); ++iter) { + std::vector<ModInfo::EFlag> 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<DirectoryEntry *>::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<bool(const QString &)> &filter) const +{ + QStringList result; + DirectoryEntry *dir = m_DirectoryStructure; + if (!path.isEmpty()) + dir = dir->findSubDirectoryRecursive(ToWString(path)); + if (dir != nullptr) { + std::vector<FileEntry::Ptr> 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<MOBase::IOrganizer::FileInfo> OrganizerCore::findFileInfos( + const QString &path, + const std::function<bool(const MOBase::IOrganizer::FileInfo &)> &filter) + const +{ + QList<IOrganizer::FileInfo> result; + DirectoryEntry *dir = m_DirectoryStructure; + if (!path.isEmpty()) + dir = dir->findSubDirectoryRecursive(ToWString(path)); + if (dir != nullptr) { + std::vector<FileEntry::Ptr> 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<MOBase::ExecutableForcedLoadSetting> &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<MOBase::ExecutableForcedLoadSetting> &forcedLibraries, + LPDWORD exitCode) +{ + HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); + if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) { + std::unique_ptr<LockedDialog> 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<MOBase::ExecutableForcedLoadSetting> &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<ExecutableForcedLoadSetting> 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<QString> 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<QString>& 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<bool(const QString &)> &func) +{ + auto conn = m_AboutToRun.connect(func); + return conn.connected(); +} + +bool OrganizerCore::onFinishedRun( + const std::function<void(const QString &, unsigned int)> &func) +{ + auto conn = m_FinishedRun.connect(func); + return conn.connected(); +} + +bool OrganizerCore::onModInstalled( + const std::function<void(const QString &)> &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<DataArchives>(); + + 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<unsigned int> modsToUpdate; + modsToUpdate.append(index); + updateModsActiveState(modsToUpdate, active); +} + +void OrganizerCore::updateModsActiveState(const QList<unsigned int> &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<unsigned int, ModInfo::Ptr> allModInfo; + allModInfo[index] = modInfo; + updateModsInDirectoryStructure(allModInfo); +} + +void OrganizerCore::updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::Ptr> 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<QString> archives = enabledArchives(); + m_DirectoryRefresher.setMods( + m_CurrentProfile->getActiveMods(), + std::set<QString>(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<MOBase::IPluginModPage>()) { + 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<QString> OrganizerCore::enabledArchives() +{ + std::vector<QString> 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<std::tuple<QString, QString, int>> activeModList + = m_CurrentProfile->getActiveMods(); + auto archives = enabledArchives(); + m_DirectoryRefresher.setMods( + activeModList, std::set<QString>(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<unsigned int> index) { + try { + QMap<unsigned int, ModInfo::Ptr> modsToEnable; + QMap<unsigned int, ModInfo::Ptr> 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<ModInfo::EFlag> 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<ScriptExtender>()) { + QString hookdll = QDir::toNativeSeparators( + managedGame()->dataDirectory().absoluteFilePath(extender->PluginPath() + "/hook.dll")); + if (QFile(hookdll).exists()) + return hookdll; + } + return QString(); +} + +std::vector<unsigned int> OrganizerCore::activeProblems() const +{ + std::vector<unsigned int> 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("<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.").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<Mapping> 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<IPluginGame *>(); + 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<LocalSavegames>(); + 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<MOBase::IPluginFileMapper>()) { + IPlugin *plugin = dynamic_cast<IPlugin *>(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<Mapping> OrganizerCore::fileMapping( + const QString &dataPath, const QString &relPath, const DirectoryEntry *base, + const DirectoryEntry *directoryEntry, int createDestination) +{ + std::vector<Mapping> 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<DirectoryEntry *>::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<Mapping> subRes = fileMapping( + dataPath, relPath + dirName + "\\", base, *current, createDestination); + result.insert(result.end(), subRes.begin(), subRes.end()); + } + return result; +} |
