#include "organizercore.h" #include "delayedfilewriter.h" #include "guessedvalue.h" #include "imodinterface.h" #include "imoinfo.h" #include "iplugingame.h" #include "iuserinterface.h" #include "loadmechanism.h" #include "messagedialog.h" #include "modlistsortproxy.h" #include "modrepositoryfileinfo.h" #include "nexusinterface.h" #include "plugincontainer.h" #include "pluginlistsortproxy.h" #include "profile.h" #include "logbuffer.h" #include "credentialsdialog.h" #include "filedialogmemory.h" #include "modinfodialog.h" #include "spawn.h" #include "syncoverwritedialog.h" #include "nxmaccessmanager.h" #include #include #include #include #include #include #include #include "appconfig.h" #include #include #include "lockeddialog.h" #include "instancemanager.h" #include #include "helper.h" #include #include #include #include #include #include #include #include #include #include #include #include // for qUtf8Printable, etc #include #include #include #include // for _tcsicmp #include #include #include // for memset, wcsrchr #include #include #include #include #include #include //for wstring #include #include using namespace MOShared; using namespace MOBase; //static CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None; static bool isOnline() { QList interfaces = QNetworkInterface::allInterfaces(); bool connected = false; for (auto iter = interfaces.begin(); iter != interfaces.end() && !connected; ++iter) { if ((iter->flags() & QNetworkInterface::IsUp) && (iter->flags() & QNetworkInterface::IsRunning) && !(iter->flags() & QNetworkInterface::IsLoopBack)) { auto addresses = iter->addressEntries(); if (addresses.count() == 0) { continue; } qDebug("interface %s seems to be up (address: %s)", qUtf8Printable(iter->humanReadableName()), qUtf8Printable(addresses[0].ip().toString())); connected = true; } } return connected; } static bool renameFile(const QString &oldName, const QString &newName, bool overwrite = true) { if (overwrite && QFile::exists(newName)) { QFile::remove(newName); } return QFile::rename(oldName, newName); } static std::wstring getProcessName(HANDLE process) { wchar_t buffer[MAX_PATH]; wchar_t *fileName = L"unknown"; if (process == nullptr) return fileName; if (::GetProcessImageFileNameW(process, buffer, MAX_PATH) != 0) { fileName = wcsrchr(buffer, L'\\'); if (fileName == nullptr) { fileName = buffer; } else { fileName += 1; } } return fileName; } // Get parent PID for the given process, return 0 on failure static DWORD getProcessParentID(DWORD pid) { HANDLE th = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe = { 0 }; pe.dwSize = sizeof(PROCESSENTRY32); DWORD res = 0; if (Process32First(th, &pe)) do { if (pe.th32ProcessID == pid) { res = pe.th32ParentProcessID; break; } } while (Process32Next(th, &pe)); CloseHandle(th); return res; } static void startSteam(QWidget *widget) { QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam", QSettings::NativeFormat); QString exe = steamSettings.value("SteamExe", "").toString(); if (!exe.isEmpty()) { exe = QString("\"%1\"").arg(exe); // See if username and password supplied. If so, pass them into steam. QStringList args; QString username; QString password; if (Settings::instance().getSteamLogin(username, password)) { args << "-login"; args << username; if (password != "") { args << password; } } if (!QProcess::startDetached(exe, args)) { reportError(QObject::tr("Failed to start \"%1\"").arg(exe)); } else { QMessageBox::information( widget, QObject::tr("Waiting"), QObject::tr("Please press OK once you're logged into steam.")); } } } template QStringList toStringList(InputIterator current, InputIterator end) { QStringList result; for (; current != end; ++current) { result.append(*current); } return result; } bool checkService() { SC_HANDLE serviceManagerHandle = NULL; SC_HANDLE serviceHandle = NULL; LPSERVICE_STATUS_PROCESS serviceStatus = NULL; LPQUERY_SERVICE_CONFIG serviceConfig = NULL; bool serviceRunning = true; DWORD bytesNeeded; try { serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); if (!serviceManagerHandle) { qWarning("failed to open service manager (query status) (error %d)", GetLastError()); throw 1; } serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); if (!serviceHandle) { qWarning("failed to open EventLog service (query status) (error %d)", GetLastError()); throw 2; } if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded) || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { qWarning("failed to get size of service config (error %d)", GetLastError()); throw 3; } DWORD serviceConfigSize = bytesNeeded; serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize); if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) { qWarning("failed to query service config (error %d)", GetLastError()); throw 4; } if (serviceConfig->dwStartType == SERVICE_DISABLED) { qCritical("Windows Event Log service is disabled!"); serviceRunning = false; } if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded) || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { qWarning("failed to get size of service status (error %d)", GetLastError()); throw 5; } DWORD serviceStatusSize = bytesNeeded; serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize); if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) { qWarning("failed to query service status (error %d)", GetLastError()); throw 6; } if (serviceStatus->dwCurrentState != SERVICE_RUNNING) { qCritical("Windows Event Log service is not running"); serviceRunning = false; } } catch (int e) { UNUSED_VAR(e); serviceRunning = false; } if (serviceStatus) { LocalFree(serviceStatus); } if (serviceConfig) { LocalFree(serviceConfig); } if (serviceHandle) { CloseServiceHandle(serviceHandle); } if (serviceManagerHandle) { CloseServiceHandle(serviceManagerHandle); } return serviceRunning; } OrganizerCore::OrganizerCore(const QSettings &initSettings) : m_UserInterface(nullptr) , m_PluginContainer(nullptr) , m_GameName() , m_CurrentProfile(nullptr) , m_Settings(initSettings) , m_Updater(NexusInterface::instance(m_PluginContainer)) , m_AboutToRun() , m_FinishedRun() , m_ModInstalled() , m_ModList(m_PluginContainer, this) , m_PluginList(this) , m_DirectoryRefresher() , m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0)) , m_DownloadManager(NexusInterface::instance(m_PluginContainer), this) , m_InstallationManager() , m_RefresherThread() , m_DirectoryUpdate(false) , m_ArchivesInit(false) , m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this)) { m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers()); NexusInterface::instance(m_PluginContainer)->setCacheDirectory(m_Settings.getCacheDirectory()); 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(validateSuccessful(bool)), this, SLOT(loginSuccessful(bool))); connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(loginFailed(QString))); // This seems awfully imperative connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), &m_Settings, SLOT(managedGameChanged(MOBase::IPluginGame const *))); connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), &m_DownloadManager, SLOT(managedGameChanged(MOBase::IPluginGame const *))); connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)), &m_PluginList, SLOT(managedGameChanged(MOBase::IPluginGame const *))); connect(&m_PluginList, &PluginList::writePluginsList, &m_PluginListsWriter, &DelayedFileWriterBase::write); // make directory refresher run in a separate thread m_RefresherThread.start(); m_DirectoryRefresher.moveToThread(&m_RefresherThread); } OrganizerCore::~OrganizerCore() { m_RefresherThread.exit(); m_RefresherThread.wait(); prepareStart(); // profile has to be cleaned up before the modinfo-buffer is cleared delete m_CurrentProfile; m_CurrentProfile = nullptr; ModInfo::clear(); LogBuffer::cleanQuit(); m_ModList.setProfile(nullptr); // NexusInterface::instance()->cleanup(); delete m_DirectoryStructure; } QString OrganizerCore::commitSettings(const QString &iniFile) { if (!shellRename(iniFile + ".new", iniFile, true, qApp->activeWindow())) { DWORD err = ::GetLastError(); // make a second attempt using qt functions but if that fails print the // error from the first attempt if (!renameFile(iniFile + ".new", iniFile)) { return windowsErrorString(err); } } return QString(); } QSettings::Status OrganizerCore::storeSettings(const QString &fileName) { QSettings settings(fileName, QSettings::IniFormat); if (m_UserInterface != nullptr) { m_UserInterface->storeSettings(settings); } if (m_CurrentProfile != nullptr) { settings.setValue("selected_profile", m_CurrentProfile->name().toUtf8().constData()); } settings.remove("customExecutables"); settings.beginWriteArray("customExecutables"); std::vector::const_iterator current, end; m_ExecutablesList.getExecutables(current, end); int count = 0; for (; current != end; ++current) { const Executable &item = *current; settings.setArrayIndex(count++); settings.setValue("title", item.m_Title); settings.setValue("custom", item.isCustom()); settings.setValue("toolbar", item.isShownOnToolbar()); settings.setValue("ownicon", item.usesOwnIcon()); if (item.isCustom()) { settings.setValue("binary", item.m_BinaryInfo.absoluteFilePath()); settings.setValue("arguments", item.m_Arguments); settings.setValue("workingDirectory", item.m_WorkingDirectory); settings.setValue("steamAppID", item.m_SteamAppID); } } settings.endArray(); FileDialogMemory::save(settings); settings.sync(); return settings.status(); } void OrganizerCore::storeSettings() { QString iniFile = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()); if (QFileInfo(iniFile).exists()) { if (!shellCopy(iniFile, iniFile + ".new", true, qApp->activeWindow())) { QMessageBox::critical( qApp->activeWindow(), tr("Failed to write settings"), tr("An error occured trying to update MO settings to %1: %2") .arg(iniFile, windowsErrorString(::GetLastError()))); return; } } QString writeTarget = iniFile + ".new"; QSettings::Status result = storeSettings(writeTarget); if (result == QSettings::NoError) { QString errMsg = commitSettings(iniFile); if (!errMsg.isEmpty()) { qWarning("settings file not writable, may be locked by another " "application, trying direct write"); writeTarget = iniFile; result = storeSettings(iniFile); } } if (result != QSettings::NoError) { QString reason = result == QSettings::AccessError ? tr("File is write protected") : result == QSettings::FormatError ? tr("Invalid file format (probably a bug)") : tr("Unknown error %1").arg(result); QMessageBox::critical( qApp->activeWindow(), tr("Failed to write settings"), tr("An error occured trying to write back MO settings to %1: %2") .arg(writeTarget, reason)); } } bool OrganizerCore::testForSteam(bool *found, bool *access) { HANDLE hProcessSnap; HANDLE hProcess; PROCESSENTRY32 pe32; DWORD lastError; if (found == nullptr || access == nullptr) { return false; } // Take a snapshot of all processes in the system. hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hProcessSnap == INVALID_HANDLE_VALUE) { lastError = GetLastError(); qCritical("unable to get snapshot of processes (error %d)", lastError); return false; } // Retrieve information about the first process, // and exit if unsuccessful pe32.dwSize = sizeof(PROCESSENTRY32); if (!Process32First(hProcessSnap, &pe32)) { lastError = GetLastError(); qCritical("unable to get first process (error %d)", lastError); CloseHandle(hProcessSnap); return false; } *found = false; *access = true; // Now walk the snapshot of processes, and // display information about each process in turn do { if ((_tcsicmp(pe32.szExeFile, L"Steam.exe") == 0) || (_tcsicmp(pe32.szExeFile, L"SteamService.exe") == 0)) { *found = true; // Try to open the process to determine if MO has the proper access hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pe32.th32ProcessID); if (hProcess == NULL) { lastError = GetLastError(); if (lastError == ERROR_ACCESS_DENIED) { *access = false; } } else { CloseHandle(hProcess); } break; } } while(Process32Next(hProcessSnap, &pe32)); CloseHandle(hProcessSnap); return true; } 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 (m_Settings.getNexusApiKey(apiKey)) { // credentials stored or user entered them manually qDebug("attempt to verify nexus api key"); accessManager->apiCheck(apiKey); return true; } else { // no credentials stored and user didn't enter them accessManager->refuseValidation(); return false; } } } void OrganizerCore::startMOUpdate() { if (nexusApi()) { m_PostLoginTasks.append([&]() { m_Updater.startUpdate(); }); } else { m_Updater.startUpdate(); } } void OrganizerCore::downloadRequestedNXM(const QString &url) { qDebug("download requested: %s", qUtf8Printable(url)); if (nexusApi()) { m_PendingDownloads.append(url); } else { m_DownloadManager.addNXMDownload(url); } } void OrganizerCore::externalMessage(const QString &message) { if (MOShortcut moshortcut{ message } ) { if(moshortcut.hasExecutable()) runShortcut(moshortcut); } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); downloadRequestedNXM(message); } } void OrganizerCore::downloadRequested(QNetworkReply *reply, QString gameName, int modID, const QString &fileName) { try { if (m_DownloadManager.addDownload(reply, QStringList(), fileName, gameName, modID, 0, new ModRepositoryFileInfo(gameName, modID))) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); } } catch (const std::exception &e) { MessageDialog::showMessage(tr("Download failed"), qApp->activeWindow()); qCritical("exception starting download: %s", e.what()); } } void OrganizerCore::removeOrigin(const QString &name) { FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(name)); origin.enable(false); refreshLists(); } void OrganizerCore::downloadSpeed(const QString &serverName, int bytesPerSecond) { m_Settings.setDownloadSpeed(serverName, bytesPerSecond); } InstallationManager *OrganizerCore::installationManager() { return &m_InstallationManager; } bool OrganizerCore::createDirectory(const QString &path) { if (!QDir(path).exists() && !QDir().mkpath(path)) { QMessageBox::critical(nullptr, QObject::tr("Error"), QObject::tr("Failed to create \"%1\". Your user " "account probably lacks permission.") .arg(QDir::toNativeSeparators(path))); return false; } else { return true; } } bool OrganizerCore::bootstrap() { return createDirectory(m_Settings.getProfileDirectory()) && createDirectory(m_Settings.getModDirectory()) && createDirectory(m_Settings.getDownloadDirectory()) && createDirectory(m_Settings.getOverwriteDirectory()) && createDirectory(QString::fromStdWString(crashDumpsPath())) && cycleDiagnostics(); } void OrganizerCore::createDefaultProfile() { QString profilesPath = settings().getProfileDirectory(); if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size() == 0) { Profile newProf("Default", managedGame(), false); } } void OrganizerCore::prepareVFS() { m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString())); } void OrganizerCore::updateVFSParams(int logLevel, int crashDumpsType, QString executableBlacklist) { setGlobalCrashDumpsType(crashDumpsType); m_USVFS.updateParams(logLevel, crashDumpsType, executableBlacklist); } bool OrganizerCore::cycleDiagnostics() { if (int maxDumps = settings().crashDumpsMax()) removeOldFiles(QString::fromStdWString(crashDumpsPath()), "*.dmp", maxDumps, QDir::Time|QDir::Reversed); return true; } //static void OrganizerCore::setGlobalCrashDumpsType(int crashDumpsType) { m_globalCrashDumpsType = ::crashDumpsType(crashDumpsType); } //static std::wstring OrganizerCore::crashDumpsPath() { return ( qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::dumpsDir()) ).toStdWString(); } bool OrganizerCore::getArchiveParsing() const { return m_ArchiveParsing; } void OrganizerCore::setArchiveParsing(const bool archiveParsing) { m_ArchiveParsing = archiveParsing; } void OrganizerCore::setCurrentProfile(const QString &profileName) { if ((m_CurrentProfile != nullptr) && (profileName == m_CurrentProfile->name())) { return; } QDir profileBaseDir(settings().getProfileDirectory()); QString profileDir = profileBaseDir.absoluteFilePath(profileName); if (!QDir(profileDir).exists()) { // selected profile doesn't exist. Ensure there is at least one profile, // then pick any one createDefaultProfile(); profileDir = profileBaseDir.absoluteFilePath( profileBaseDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot).at(0)); } Profile *newProfile = new Profile(QDir(profileDir), managedGame()); delete m_CurrentProfile; m_CurrentProfile = newProfile; m_ModList.setProfile(newProfile); if (m_CurrentProfile->invalidationActive(nullptr)) { m_CurrentProfile->activateInvalidation(); } else { m_CurrentProfile->deactivateInvalidation(); } connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this, SLOT(modStatusChanged(uint))); connect(m_CurrentProfile, SIGNAL(modStatusChanged(QList)), this, SLOT(modStatusChanged(QList))); refreshDirectoryStructure(); //This line is not actually needed and was only added to allow some //outside detection of Mo2 profile change. (like BaobobMiller utility) if (m_CurrentProfile != nullptr) { settings().directInterface().setValue("selected_profile", m_CurrentProfile->name().toUtf8().constData()); } } MOBase::IModRepositoryBridge *OrganizerCore::createNexusBridge() const { return new NexusBridge(m_PluginContainer); } QString OrganizerCore::profileName() const { if (m_CurrentProfile != nullptr) { return m_CurrentProfile->name(); } else { return ""; } } QString OrganizerCore::profilePath() const { if (m_CurrentProfile != nullptr) { return m_CurrentProfile->absolutePath(); } else { return ""; } } QString OrganizerCore::downloadsPath() const { return QDir::fromNativeSeparators(m_Settings.getDownloadDirectory()); } QString OrganizerCore::overwritePath() const { return QDir::fromNativeSeparators(m_Settings.getOverwriteDirectory()); } QString OrganizerCore::basePath() const { return QDir::fromNativeSeparators(m_Settings.getBaseDirectory()); } QString OrganizerCore::modsPath() const { return QDir::fromNativeSeparators(m_Settings.getModDirectory()); } MOBase::VersionInfo OrganizerCore::appVersion() const { return m_Updater.getVersion(); } MOBase::IModInterface *OrganizerCore::getMod(const QString &name) const { unsigned int index = ModInfo::getIndex(name); return index == UINT_MAX ? nullptr : ModInfo::getByIndex(index).data(); } MOBase::IPluginGame *OrganizerCore::getGame(const QString &name) const { for (IPluginGame *game : m_PluginContainer->plugins()) { if (game != nullptr && game->gameShortName().compare(name, Qt::CaseInsensitive) == 0) return game; } return nullptr; } MOBase::IModInterface *OrganizerCore::createMod(GuessedValue &name) { bool merge = false; if (!m_InstallationManager.testOverwrite(name, &merge)) { return nullptr; } m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); QString targetDirectory = QDir::fromNativeSeparators(m_Settings.getModDirectory()) .append("/") .append(name); QSettings settingsFile(targetDirectory + "/meta.ini", QSettings::IniFormat); if (!merge) { settingsFile.setValue("modid", 0); settingsFile.setValue("version", ""); settingsFile.setValue("newestVersion", ""); settingsFile.setValue("category", 0); settingsFile.setValue("installationFile", ""); settingsFile.remove("installedFiles"); settingsFile.beginWriteArray("installedFiles", 0); settingsFile.endArray(); } return ModInfo::createFrom(m_PluginContainer, m_GamePlugin, QDir(targetDirectory), &m_DirectoryStructure) .data(); } bool OrganizerCore::removeMod(MOBase::IModInterface *mod) { unsigned int index = ModInfo::getIndex(mod->name()); if (index == UINT_MAX) { return mod->remove(); } else { return ModInfo::removeMod(index); } } void OrganizerCore::modDataChanged(MOBase::IModInterface *) { refreshModList(false); } QVariant OrganizerCore::pluginSetting(const QString &pluginName, const QString &key) const { return m_Settings.pluginSetting(pluginName, key); } void OrganizerCore::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value) { m_Settings.setPluginSetting(pluginName, key, value); } QVariant OrganizerCore::persistent(const QString &pluginName, const QString &key, const QVariant &def) const { return m_Settings.pluginPersistent(pluginName, key, def); } void OrganizerCore::setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync) { m_Settings.setPluginPersistent(pluginName, key, value, sync); } QString OrganizerCore::pluginDataPath() const { return qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()) + "/data"; } MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName, const QString &initModName) { if (m_CurrentProfile == nullptr) { return nullptr; } if (m_InstallationManager.isRunning()) { QMessageBox::information( qApp->activeWindow(), tr("Installation cancelled"), tr("Another installation is currently in progress."), QMessageBox::Ok); return nullptr; } bool hasIniTweaks = false; GuessedValue modName; if (!initModName.isEmpty()) { modName.update(initModName, GUESS_USER); } m_CurrentProfile->writeModlistNow(); m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { MessageDialog::showMessage(tr("Installation successful"), qApp->activeWindow()); refreshModList(); int modIndex = ModInfo::getIndex(modName); if (modIndex != UINT_MAX) { ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); if (hasIniTweaks && (m_UserInterface != nullptr) && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), tr("This mod contains ini tweaks. Do you " "want to configure them now?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { m_UserInterface->displayModInformation(modInfo, modIndex, ModInfoDialog::TAB_INIFILES); } m_ModInstalled(modName); m_DownloadManager.markInstalled(fileName); emit modInstalled(modName); return modInfo.data(); } else { reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); } } else if (m_InstallationManager.wasCancelled()) { QMessageBox::information(qApp->activeWindow(), tr("Installation cancelled"), tr("The mod was not installed completely."), QMessageBox::Ok); } return nullptr; } void OrganizerCore::installDownload(int index) { if (m_InstallationManager.isRunning()) { QMessageBox::information( qApp->activeWindow(), tr("Installation cancelled"), tr("Another installation is currently in progress."), QMessageBox::Ok); return; } try { QString fileName = m_DownloadManager.getFilePath(index); QString gameName = m_DownloadManager.getGameName(index); int modID = m_DownloadManager.getModID(index); int fileID = m_DownloadManager.getFileInfo(index)->fileID; GuessedValue modName; // see if there already are mods with the specified mod id if (modID != 0) { std::vector modInfo = ModInfo::getByModID(gameName, modID); for (auto iter = modInfo.begin(); iter != modInfo.end(); ++iter) { std::vector flags = (*iter)->getFlags(); if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end()) { modName.update((*iter)->name(), GUESS_PRESET); (*iter)->saveMeta(); } } } m_CurrentProfile->writeModlistNow(); bool hasIniTweaks = false; m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { MessageDialog::showMessage(tr("Installation successful"), qApp->activeWindow()); refreshModList(); int modIndex = ModInfo::getIndex(modName); if (modIndex != UINT_MAX) { ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); modInfo->addInstalledFile(modID, fileID); if (hasIniTweaks && m_UserInterface != nullptr && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), tr("This mod contains ini tweaks. Do you " "want to configure them now?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { m_UserInterface->displayModInformation(modInfo, modIndex, ModInfoDialog::TAB_INIFILES); } m_ModInstalled(modName); } else { reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); } m_DownloadManager.markInstalled(index); emit modInstalled(modName); } else if (m_InstallationManager.wasCancelled()) { QMessageBox::information( qApp->activeWindow(), tr("Installation cancelled"), tr("The mod was not installed completely."), QMessageBox::Ok); } } catch (const std::exception &e) { reportError(e.what()); } } QString OrganizerCore::resolvePath(const QString &fileName) const { if (m_DirectoryStructure == nullptr) { return QString(); } const FileEntry::Ptr file = m_DirectoryStructure->searchFile(ToWString(fileName), nullptr); if (file.get() != nullptr) { return ToQString(file->getFullPath()); } else { return QString(); } } QStringList OrganizerCore::listDirectories(const QString &directoryName) const { QStringList result; DirectoryEntry *dir = m_DirectoryStructure; if (!directoryName.isEmpty()) dir = dir->findSubDirectoryRecursive(ToWString(directoryName)); if (dir != nullptr) { std::vector::iterator current, end; dir->getSubDirectories(current, end); for (; current != end; ++current) { result.append(ToQString((*current)->getName())); } } return result; } QStringList OrganizerCore::findFiles( const QString &path, const std::function &filter) const { QStringList result; DirectoryEntry *dir = m_DirectoryStructure; if (!path.isEmpty()) dir = dir->findSubDirectoryRecursive(ToWString(path)); if (dir != nullptr) { std::vector files = dir->getFiles(); foreach (FileEntry::Ptr file, files) { if (filter(ToQString(file->getFullPath()))) { result.append(ToQString(file->getFullPath())); } } } return result; } QStringList OrganizerCore::getFileOrigins(const QString &fileName) const { QStringList result; const FileEntry::Ptr file = m_DirectoryStructure->searchFile(ToWString(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())); } } return result; } QList OrganizerCore::findFileInfos( const QString &path, const std::function &filter) const { QList result; DirectoryEntry *dir = m_DirectoryStructure; if (!path.isEmpty()) dir = dir->findSubDirectoryRecursive(ToWString(path)); if (dir != nullptr) { std::vector files = dir->getFiles(); foreach (FileEntry::Ptr file, files) { IOrganizer::FileInfo info; info.filePath = ToQString(file->getFullPath()); bool fromArchive = false; info.origins.append(ToQString( m_DirectoryStructure->getOriginByID(file->getOrigin(fromArchive)) .getName())); info.archive = fromArchive ? ToQString(file->getArchive().first) : ""; foreach (auto idx, file->getAlternatives()) { info.origins.append( ToQString(m_DirectoryStructure->getOriginByID(idx.first).getName())); } if (filter(info)) { result.append(info); } } } return result; } DownloadManager *OrganizerCore::downloadManager() { return &m_DownloadManager; } PluginList *OrganizerCore::pluginList() { return &m_PluginList; } ModList *OrganizerCore::modList() { return &m_ModList; } QStringList OrganizerCore::modsSortedByProfilePriority() const { QStringList res; for (int i = currentProfile()->getPriorityMinimum(); i < currentProfile()->getPriorityMinimum() + (int)currentProfile()->numRegularMods(); ++i) { int modIndex = currentProfile()->modIndexByPriority(i); auto modInfo = ModInfo::getByIndex(modIndex); if (!modInfo->hasFlag(ModInfo::FLAG_OVERWRITE) && !modInfo->hasFlag(ModInfo::FLAG_BACKUP)) { res.push_back(ModInfo::getByIndex(modIndex)->name()); } } return res; } void OrganizerCore::spawnBinary(const QFileInfo &binary, const QString &arguments, const QDir ¤tDirectory, const QString &steamAppID, const QString &customOverwrite, const QList &forcedLibraries) { DWORD processExitCode = 0; HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, customOverwrite, forcedLibraries, &processExitCode); if (processHandle != INVALID_HANDLE_VALUE) { refreshDirectoryStructure(); // need to remove our stored load order because it may be outdated if a foreign tool changed the // file time. After removing that file, refreshESPList will use the file time as the order if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) { qDebug("removing loadorder.txt"); QFile::remove(m_CurrentProfile->getLoadOrderFileName()); } refreshDirectoryStructure(); refreshESPList(true); savePluginList(); //These callbacks should not fiddle with directoy structure and ESPs. m_FinishedRun(binary.absoluteFilePath(), processExitCode); } } HANDLE OrganizerCore::spawnBinaryDirect(const QFileInfo &binary, const QString &arguments, const QString &profileName, const QDir ¤tDirectory, const QString &steamAppID, const QString &customOverwrite, const QList &forcedLibraries, LPDWORD exitCode) { HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) { std::unique_ptr dlg; ILockedWaitingForProcess* uilock = nullptr; if (m_UserInterface != nullptr) { uilock = m_UserInterface->lock(); } else { // i.e. when running command line shortcuts there is no m_UserInterface dlg.reset(new LockedDialog); dlg->show(); dlg->setEnabled(true); uilock = dlg.get(); } ON_BLOCK_EXIT([&]() { if (m_UserInterface != nullptr) { m_UserInterface->unlock(); } }); DWORD ignoreExitCode; waitForProcessCompletion(processHandle, exitCode ? exitCode : &ignoreExitCode, uilock); cycleDiagnostics(); } return processHandle; } HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, const QString &arguments, const QString &profileName, const QDir ¤tDirectory, const QString &steamAppID, const QString &customOverwrite, const QList &forcedLibraries) { prepareStart(); if (!binary.exists()) { reportError( tr("Executable not found: %1").arg(qUtf8Printable(binary.absoluteFilePath()))); return INVALID_HANDLE_VALUE; } if (!steamAppID.isEmpty()) { ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str()); } else { ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(m_Settings.getSteamAppID()).c_str()); } QWidget *window = qApp->activeWindow(); if ((window != nullptr) && (!window->isVisible())) { window = nullptr; } // This could possibly be extracted somewhere else but it's probably for when // we have more than one provider of game registration. if ((QFileInfo( managedGame()->gameDirectory().absoluteFilePath("steam_api.dll")) .exists() || QFileInfo(managedGame()->gameDirectory().absoluteFilePath( "steam_api64.dll")) .exists()) && (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) { bool steamFound = true; bool steamAccess = true; if (!testForSteam(&steamFound, &steamAccess)) { qCritical("unable to determine state of Steam"); } if (!steamFound) { 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); // double-check that Steam is started and MO has access steamFound = true; steamAccess = true; if (!testForSteam(&steamFound, &steamAccess)) { qCritical("unable to determine state of Steam"); } else if (!steamFound) { qCritical("could not find Steam"); } } else if (result == QDialogButtonBox::Cancel) { return INVALID_HANDLE_VALUE; } } if (!steamAccess) { QDialogButtonBox::StandardButton result; result = QuestionBoxMemory::query(window, "steamAdminQuery", binary.fileName(), tr("Steam: Access Denied"), tr("MO was denied access to the Steam process. This normally indicates that " "Steam is being run as administrator while MO is not. This can cause issues " "launching the game. It is recommended to not run Steam as administrator unless " "absolutely neccessary.\n\n" "Restart MO as administrator?"), QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); if (result == QDialogButtonBox::Yes) { WCHAR cwd[MAX_PATH]; if (!GetCurrentDirectory(MAX_PATH, cwd)) { qCritical("unable to get current directory (error %d)", GetLastError()); cwd[0] = L'\0'; } if (!Helper::adminLaunch( qApp->applicationDirPath().toStdWString(), qApp->applicationFilePath().toStdWString(), std::wstring(cwd))) { qCritical("unable to relaunch MO as admin"); return INVALID_HANDLE_VALUE; } qApp->exit(0); return INVALID_HANDLE_VALUE; } 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, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite) { QFileInfo binary; QString arguments = args.join(" "); QString currentDirectory = cwd; QString profileName = profile; if (profile.length() == 0) { if (m_CurrentProfile != nullptr) { profileName = m_CurrentProfile->name(); } else { throw MyException(tr("No profile set")); } } QString steamAppID; QString customOverwrite; QList forcedLibraries; if (executable.contains('\\') || executable.contains('/')) { // file path binary = QFileInfo(executable); if (binary.isRelative()) { // relative path, should be relative to game directory binary = QFileInfo( managedGame()->gameDirectory().absoluteFilePath(executable)); } if (cwd.length() == 0) { currentDirectory = binary.absolutePath(); } try { const Executable &exe = m_ExecutablesList.findByBinary(binary); steamAppID = exe.m_SteamAppID; customOverwrite = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) .toString(); if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); } } catch (const std::runtime_error &) { // nop } } else { // only a file name, search executables list try { const Executable &exe = m_ExecutablesList.find(executable); steamAppID = exe.m_SteamAppID; customOverwrite = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) .toString(); if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); } if (arguments == "") { arguments = exe.m_Arguments; } binary = exe.m_BinaryInfo; if (cwd.length() == 0) { currentDirectory = exe.m_WorkingDirectory; } } catch (const std::runtime_error &) { qWarning("\"%s\" not set up as executable", qUtf8Printable(executable)); binary = QFileInfo(executable); } } if (!forcedCustomOverwrite.isEmpty()) customOverwrite = forcedCustomOverwrite; if (ignoreCustomOverwrite) customOverwrite.clear(); return spawnBinaryDirect(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); } bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode) { if (!Settings::instance().lockGUI()) return true; ILockedWaitingForProcess* uilock = nullptr; if (m_UserInterface != nullptr) { uilock = m_UserInterface->lock(); } ON_BLOCK_EXIT([&] () { if (m_UserInterface != nullptr) { m_UserInterface->unlock(); } }); return waitForProcessCompletion(handle, exitCode, uilock); } bool OrganizerCore::waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) { bool originalHandle = true; bool newHandle = true; bool uiunlocked = false; DWORD currentPID = 0; QString processName; auto waitForChildUntil = GetTickCount64(); if (handle != INVALID_HANDLE_VALUE) { currentPID = GetProcessId(handle); processName = QString::fromStdWString(getProcessName(handle)); } // Certain process names we wish to "hide" for aesthetic reason: bool waitingOnHidden = false; std::vector hiddenList; hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); for (QString hide : hiddenList) if (processName.contains(hide, Qt::CaseInsensitive)) waitingOnHidden = true; // The main reason for adding the hidden list is to hide the MO proxy we use to spawn virtualized processes. // On the one hand we want to display the real executable without it feeling laggy, on the other we don't want // to requery processes all the time if for some reason we are waiting on hidden processes and find no "unhidden" // process. For this reason we use exponential backoff and also start with a delibrately low value to improve // the responsiveness of the initial update DWORD64 nextHiddenCheck = GetTickCount64(); DWORD64 nextHiddenCheckDelay = 50; constexpr DWORD INPUT_EVENT = WAIT_OBJECT_0 + 1; DWORD res = WAIT_TIMEOUT; while (handle != INVALID_HANDLE_VALUE && (newHandle || res == WAIT_TIMEOUT || res == INPUT_EVENT)) { if (newHandle) { processName += QString(" (%1)").arg(currentPID); if (uilock) uilock->setProcessName(processName); qDebug() << "Waiting for" << (originalHandle ? "spawned" : "usvfs") << "process completion :" << qUtf8Printable(processName); newHandle = false; } // Wait for a an event on the handle, a key press, mouse click or timeout res = MsgWaitForMultipleObjects(1, &handle, FALSE, 200, QS_KEY | QS_MOUSEBUTTON); if (res == WAIT_FAILED) { qWarning() << "Failed waiting for process completion : MsgWaitForMultipleObjects WAIT_FAILED" << GetLastError(); break; } // keep processing events so the app doesn't appear dead QCoreApplication::sendPostedEvents(); QCoreApplication::processEvents(); if (uilock && uilock->unlockForced()) { uiunlocked = true; break; } if (res == WAIT_OBJECT_0) { // process we were waiting on has completed if (originalHandle && exitCode && !::GetExitCodeProcess(handle, exitCode)) qWarning() << "Failed getting exit code of complete process :" << GetLastError(); CloseHandle(handle); handle = INVALID_HANDLE_VALUE; originalHandle = false; // if the previous process spawned a child process and immediately exits we may miss it if we check immediately waitForChildUntil = GetTickCount64() + 800; } // search for another process to wait on if either: // 1. we just completed waiting for a process and need to find/wait for an inject child // 2. we are currently waiting on a hidden process so periodically check if there is a non-hidden process to wait on bool firstIteration = true; while ((handle == INVALID_HANDLE_VALUE && GetTickCount64() <= waitForChildUntil) || (waitingOnHidden && GetTickCount64() >= nextHiddenCheck)) { if (firstIteration) firstIteration = false; else { QThread::msleep(200); QCoreApplication::sendPostedEvents(); QCoreApplication::processEvents(); } // search if there is another usvfs process active handle = findAndOpenAUSVFSProcess(hiddenList, currentPID); waitingOnHidden = false; newHandle = handle != INVALID_HANDLE_VALUE; if (newHandle) { currentPID = GetProcessId(handle); processName = QString::fromStdWString(getProcessName(handle)); for (QString hide : hiddenList) if (processName.contains(hide, Qt::CaseInsensitive)) waitingOnHidden = true; } if (waitingOnHidden) { nextHiddenCheck = GetTickCount64() + nextHiddenCheckDelay; nextHiddenCheckDelay = std::min(nextHiddenCheckDelay * 2, (DWORD64) 2000); } else { nextHiddenCheck = GetTickCount64(); nextHiddenCheckDelay = 200; } } } if (res == WAIT_OBJECT_0) qDebug() << "Waiting for process completion successfull"; else if (uiunlocked) qDebug() << "Waiting for process completion aborted by UI"; else qDebug() << "Waiting for process completion not successfull :" << res; if (handle != INVALID_HANDLE_VALUE) ::CloseHandle(handle); return res == WAIT_OBJECT_0; } HANDLE OrganizerCore::findAndOpenAUSVFSProcess(const std::vector& hiddenList, DWORD preferedParentPid) { // for practical reasons a querySize of 1 is probably enough, we use a larger query as a heuristics // to find a more "aesthetic injected processes (attempting to comply to hiddenList and preferedParentPid) constexpr size_t querySize = 100; DWORD pids[querySize]; size_t found = querySize; if (!::GetVFSProcessList(&found, pids)) { qWarning() << "Failed seeking USVFS processes : GetVFSProcessList failed?!"; return INVALID_HANDLE_VALUE; } HANDLE best_match = INVALID_HANDLE_VALUE; bool best_match_hidden = true; for (size_t i = 0; i < found; ++i) { if (pids[i] == GetCurrentProcessId()) continue; // obviously don't wait for MO process HANDLE handle = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pids[i]); if (handle == INVALID_HANDLE_VALUE) { qWarning() << "Failed openning USVFS process " << pids[i] << " : OpenProcess failed" << GetLastError(); continue; } QString pname = QString::fromStdWString(getProcessName(handle)); bool phidden = false; for (auto hide : hiddenList) if (pname.contains(hide, Qt::CaseInsensitive)) phidden = true; bool pprefered = preferedParentPid && getProcessParentID(pids[i]) == preferedParentPid; if (best_match == INVALID_HANDLE_VALUE || best_match_hidden || (!phidden && pprefered)) { if (best_match != INVALID_HANDLE_VALUE) CloseHandle(best_match); best_match = handle; best_match_hidden = phidden; } else CloseHandle(handle); if (!phidden && pprefered) return best_match; } return best_match; } bool OrganizerCore::onAboutToRun( const std::function &func) { auto conn = m_AboutToRun.connect(func); return conn.connected(); } bool OrganizerCore::onFinishedRun( const std::function &func) { auto conn = m_FinishedRun.connect(func); return conn.connected(); } bool OrganizerCore::onModInstalled( const std::function &func) { auto conn = m_ModInstalled.connect(func); return conn.connected(); } void OrganizerCore::refreshModList(bool saveChanges) { // don't lose changes! if (saveChanges) { m_CurrentProfile->writeModlistNow(true); } ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, m_PluginContainer, m_Settings.displayForeign(), managedGame()); m_CurrentProfile->refreshModStatus(); m_ModList.notifyChange(-1); refreshDirectoryStructure(); } void OrganizerCore::refreshESPList(bool force) { if (m_DirectoryUpdate) { // don't mess up the esp list if we're currently updating the directory // structure m_PostRefreshTasks.append([=]() { this->refreshESPList(force); }); return; } m_CurrentProfile->writeModlist(); // clear list try { m_PluginList.refresh(m_CurrentProfile->name(), *m_DirectoryStructure, m_CurrentProfile->getLockedOrderFileName(), force); } catch (const std::exception &e) { reportError(tr("Failed to refresh list of esps: %1").arg(e.what())); } } void OrganizerCore::refreshBSAList() { DataArchives *archives = m_GamePlugin->feature(); if (archives != nullptr) { m_ArchivesInit = false; // default archives are the ones enabled outside MO. if the list can't be // found (which might // happen if ini files are missing) use hard-coded defaults (preferrably the // same the game would use) m_DefaultArchives = archives->archives(m_CurrentProfile); if (m_DefaultArchives.length() == 0) { m_DefaultArchives = archives->vanillaArchives(); } m_ActiveArchives.clear(); auto iter = enabledArchives(); m_ActiveArchives = toStringList(iter.begin(), iter.end()); if (m_ActiveArchives.isEmpty()) { m_ActiveArchives = m_DefaultArchives; } if (m_UserInterface != nullptr) { m_UserInterface->updateBSAList(m_DefaultArchives, m_ActiveArchives); } m_ArchivesInit = true; } } void OrganizerCore::refreshLists() { if ((m_CurrentProfile != nullptr) && m_DirectoryStructure->isPopulated()) { refreshESPList(true); refreshBSAList(); } // no point in refreshing lists if no files have been added to the directory // tree } void OrganizerCore::updateModActiveState(int index, bool active) { QList modsToUpdate; modsToUpdate.append(index); updateModsActiveState(modsToUpdate, active); } void OrganizerCore::updateModsActiveState(const QList &modIndices, bool active) { int enabled = 0; for (auto index : modIndices) { ModInfo::Ptr modInfo = ModInfo::getByIndex(index); QDir dir(modInfo->absolutePath()); for (const QString &esm : dir.entryList(QStringList() << "*.esm", QDir::Files)) { const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esm)); if (file.get() == nullptr) { qWarning("failed to activate %s", qUtf8Printable(esm)); continue; } if (active != m_PluginList.isEnabled(esm) && file->getAlternatives().empty()) { m_PluginList.blockSignals(true); m_PluginList.enableESP(esm, active); m_PluginList.blockSignals(false); } } for (const QString &esl : dir.entryList(QStringList() << "*.esl", QDir::Files)) { const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esl)); if (file.get() == nullptr) { qWarning("failed to activate %s", qUtf8Printable(esl)); continue; } if (active != m_PluginList.isEnabled(esl) && file->getAlternatives().empty()) { m_PluginList.blockSignals(true); m_PluginList.enableESP(esl, active); m_PluginList.blockSignals(false); ++enabled; } } QStringList esps = dir.entryList(QStringList() << "*.esp", QDir::Files); for (const QString &esp : esps) { const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esp)); if (file.get() == nullptr) { qWarning("failed to activate %s", qUtf8Printable(esp)); continue; } if (active != m_PluginList.isEnabled(esp) && file->getAlternatives().empty()) { m_PluginList.blockSignals(true); m_PluginList.enableESP(esp, active); m_PluginList.blockSignals(false); ++enabled; } } } if (active && (enabled > 1)) { MessageDialog::showMessage( tr("Multiple esps/esls activated, please check that they don't conflict."), qApp->activeWindow()); } m_PluginList.refreshLoadOrder(); // immediately save affected lists m_PluginListsWriter.writeImmediately(false); } void OrganizerCore::updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo) { QMap allModInfo; allModInfo[index] = modInfo; updateModsInDirectoryStructure(allModInfo); } void OrganizerCore::updateModsInDirectoryStructure(QMap modInfo) { for (auto idx : modInfo.keys()) { // add files of the bsa to the directory structure m_DirectoryRefresher.addModFilesToStructure( m_DirectoryStructure, modInfo[idx]->name(), m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(), modInfo[idx]->stealFiles()); } DirectoryRefresher::cleanStructure(m_DirectoryStructure); // need to refresh plugin list now so we can activate esps refreshESPList(true); // activate all esps of the specified mod so the bsas get activated along with // it m_PluginList.blockSignals(true); updateModsActiveState(modInfo.keys(), true); m_PluginList.blockSignals(false); // now we need to refresh the bsa list and save it so there is no confusion // about what archives are avaiable and active refreshBSAList(); if (m_UserInterface != nullptr) { m_UserInterface->archivesWriter().writeImmediately(false); } std::vector archives = enabledArchives(); m_DirectoryRefresher.setMods( m_CurrentProfile->getActiveMods(), std::set(archives.begin(), archives.end())); // finally also add files from bsas to the directory structure for (auto idx : modInfo.keys()) { m_DirectoryRefresher.addModBSAToStructure( m_DirectoryStructure, modInfo[idx]->name(), m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(), modInfo[idx]->archives()); } } void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply) { if (m_PluginContainer != nullptr) { for (IPluginModPage *modPage : m_PluginContainer->plugins()) { ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo(); if (modPage->handlesDownload(url, reply->url(), *fileInfo)) { fileInfo->repository = modPage->name(); m_DownloadManager.addDownload(reply, fileInfo); return; } } } // no mod found that could handle the download. Is it a nexus mod? if (url.host() == "www.nexusmods.com") { QString gameName = ""; int modID = 0; int fileID = 0; QRegExp nameExp("www\\.nexusmods\\.com/(\\a+)/"); if (nameExp.indexIn(url.toString()) != -1) { gameName = nameExp.cap(1); } QRegExp modExp("mods/(\\d+)"); if (modExp.indexIn(url.toString()) != -1) { modID = modExp.cap(1).toInt(); } QRegExp fileExp("fid=(\\d+)"); if (fileExp.indexIn(reply->url().toString()) != -1) { fileID = fileExp.cap(1).toInt(); } m_DownloadManager.addDownload(reply, new ModRepositoryFileInfo(gameName, modID, fileID)); } else { if (QMessageBox::question(qApp->activeWindow(), tr("Download?"), tr("A download has been started but no installed " "page plugin recognizes it.\n" "If you download anyway no information (i.e. " "version) will be associated with the " "download.\n" "Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { m_DownloadManager.addDownload(reply, new ModRepositoryFileInfo()); } } } ModListSortProxy *OrganizerCore::createModListProxyModel() { ModListSortProxy *result = new ModListSortProxy(m_CurrentProfile, this); result->setSourceModel(&m_ModList); return result; } PluginListSortProxy *OrganizerCore::createPluginListProxyModel() { PluginListSortProxy *result = new PluginListSortProxy(this); result->setSourceModel(&m_PluginList); return result; } IPluginGame const *OrganizerCore::managedGame() const { return m_GamePlugin; } std::vector OrganizerCore::enabledArchives() { std::vector result; if (m_ArchiveParsing) { QFile archiveFile(m_CurrentProfile->getArchivesFileName()); if (archiveFile.open(QIODevice::ReadOnly)) { while (!archiveFile.atEnd()) { result.push_back(QString::fromUtf8(archiveFile.readLine()).trimmed()); } archiveFile.close(); } } return result; } void OrganizerCore::refreshDirectoryStructure() { if (!m_DirectoryUpdate) { m_CurrentProfile->writeModlistNow(true); m_DirectoryUpdate = true; std::vector> activeModList = m_CurrentProfile->getActiveMods(); auto archives = enabledArchives(); m_DirectoryRefresher.setMods( activeModList, std::set(archives.begin(), archives.end())); QTimer::singleShot(0, &m_DirectoryRefresher, SLOT(refresh())); } } void OrganizerCore::directory_refreshed() { DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure(); Q_ASSERT(newStructure != m_DirectoryStructure); if (newStructure != nullptr) { std::swap(m_DirectoryStructure, newStructure); delete newStructure; } else { // TODO: don't know why this happens, this slot seems to get called twice // with only one emit return; } m_DirectoryUpdate = false; for (int i = 0; i < m_ModList.rowCount(); ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(i); modInfo->clearCaches(); } for (auto task : m_PostRefreshTasks) { task(); } m_PostRefreshTasks.clear(); if (m_CurrentProfile != nullptr) { refreshLists(); } } void OrganizerCore::profileRefresh() { // have to refresh mods twice (again in refreshModList), otherwise the refresh // isn't complete. Not sure why ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure, m_PluginContainer, m_Settings.displayForeign(), managedGame()); m_CurrentProfile->refreshModStatus(); refreshModList(); } void OrganizerCore::modStatusChanged(unsigned int index) { try { ModInfo::Ptr modInfo = ModInfo::getByIndex(index); if (m_CurrentProfile->modEnabled(index)) { updateModInDirectoryStructure(index, modInfo); } else { updateModActiveState(index, false); if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); origin.enable(false); } if (m_UserInterface != nullptr) { m_UserInterface->archivesWriter().write(); } } modInfo->clearCaches(); for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(i); int priority = m_CurrentProfile->getModPriority(i); if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { // priorities in the directory structure are one higher because data is // 0 m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())) .setPriority(priority + 1); } } m_DirectoryStructure->getFileRegister()->sortOrigins(); refreshLists(); } catch (const std::exception &e) { reportError(tr("failed to update mod list: %1").arg(e.what())); } } void OrganizerCore::modStatusChanged(QList index) { try { QMap modsToEnable; QMap modsToDisable; for (auto idx : index) { if (m_CurrentProfile->modEnabled(idx)) { modsToEnable[idx] = ModInfo::getByIndex(idx); } else { modsToDisable[idx] = ModInfo::getByIndex(idx); } } if (!modsToEnable.isEmpty()) { updateModsInDirectoryStructure(modsToEnable); for (auto modInfo : modsToEnable.values()) { modInfo->clearCaches(); } } if (!modsToDisable.isEmpty()) { updateModsActiveState(modsToDisable.keys(), false); for (auto idx : modsToDisable.keys()) { if (m_DirectoryStructure->originExists(ToWString(modsToDisable[idx]->name()))) { FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(modsToDisable[idx]->name())); origin.enable(false); } } if (m_UserInterface != nullptr) { m_UserInterface->archivesWriter().write(); } } for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(i); int priority = m_CurrentProfile->getModPriority(i); if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { // priorities in the directory structure are one higher because data is // 0 m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())) .setPriority(priority + 1); } } m_DirectoryStructure->getFileRegister()->sortOrigins(); refreshLists(); } catch (const std::exception &e) { reportError(tr("failed to update mod list: %1").arg(e.what())); } } void OrganizerCore::loginSuccessful(bool necessary) { if (necessary) { MessageDialog::showMessage(tr("login successful"), qApp->activeWindow()); } for (QString url : m_PendingDownloads) { downloadRequestedNXM(url); } m_PendingDownloads.clear(); for (auto task : m_PostLoginTasks) { task(); } m_PostLoginTasks.clear(); NexusInterface::instance(m_PluginContainer)->loginCompleted(); } void OrganizerCore::loginSuccessfulUpdate(bool necessary) { if (necessary) { MessageDialog::showMessage(tr("login successful"), qApp->activeWindow()); } m_Updater.startUpdate(); } void OrganizerCore::loginFailed(const QString &message) { if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"), tr("Login failed, try again?")) == QMessageBox::Yes) { if (nexusApi(true)) { return; } } if (!m_PendingDownloads.isEmpty()) { MessageDialog::showMessage( tr("login failed: %1. Download will not be associated with an account") .arg(message), qApp->activeWindow()); for (QString url : m_PendingDownloads) { downloadRequestedNXM(url); } m_PendingDownloads.clear(); } else { MessageDialog::showMessage(tr("login failed: %1").arg(message), qApp->activeWindow()); m_PostLoginTasks.clear(); } NexusInterface::instance(m_PluginContainer)->loginCompleted(); } void OrganizerCore::loginFailedUpdate(const QString &message) { MessageDialog::showMessage( tr("login failed: %1. You need to log-in with Nexus to update MO.") .arg(message), qApp->activeWindow()); } void OrganizerCore::syncOverwrite() { unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { std::vector flags = mod->getFlags(); return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure, qApp->activeWindow()); if (syncDialog.exec() == QDialog::Accepted) { syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory())); modInfo->testValid(); refreshDirectoryStructure(); } } QString OrganizerCore::oldMO1HookDll() const { if (auto extender = managedGame()->feature()) { QString hookdll = QDir::toNativeSeparators( managedGame()->dataDirectory().absoluteFilePath(extender->PluginPath() + "/hook.dll")); if (QFile(hookdll).exists()) return hookdll; } return QString(); } std::vector OrganizerCore::activeProblems() const { std::vector problems; const auto& hookdll = oldMO1HookDll(); if (!hookdll.isEmpty()) { // This warning will now be shown every time the problems are checked, which is a bit // of a "log spam". But since this is a sevre error which will most likely make the // game crash/freeze/etc. and is very hard to diagnose, this "log spam" will make it // easier for the user to notice the warning. qWarning("hook.dll found in game folder: %s", qUtf8Printable(hookdll)); problems.push_back(PROBLEM_MO1SCRIPTEXTENDERWORKAROUND); } return problems; } QString OrganizerCore::shortDescription(unsigned int key) const { switch (key) { case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: { return tr("MO1 \"Script Extender\" load mechanism has left hook.dll in your game folder"); } break; default: { return tr("Description missing"); } break; } } QString OrganizerCore::fullDescription(unsigned int key) const { switch (key) { case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: { return tr("hook.dll has been found in your game folder (right click to copy the full path). " "This is most likely a leftover of setting the ModOrganizer 1 load mechanism to \"Script Extender\", " "in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or " "manually removing the file, otherwise the game is likely to crash and burn.").arg(oldMO1HookDll()); break; } default: { return tr("Description missing"); } break; } } bool OrganizerCore::hasGuidedFix(unsigned int) const { return false; } void OrganizerCore::startGuidedFix(unsigned int) const { } bool OrganizerCore::saveCurrentLists() { if (m_DirectoryUpdate) { qWarning("not saving lists during directory update"); return false; } try { savePluginList(); if (m_UserInterface != nullptr) { m_UserInterface->archivesWriter().write(); } } catch (const std::exception &e) { reportError(tr("failed to save load order: %1").arg(e.what())); } return true; } void OrganizerCore::savePluginList() { if (m_DirectoryUpdate) { // delay save till after directory update m_PostRefreshTasks.append([this]() { this->savePluginList(); }); return; } m_PluginList.saveTo(m_CurrentProfile->getLockedOrderFileName(), m_CurrentProfile->getDeleterFileName(), m_Settings.hideUncheckedPlugins()); m_PluginList.saveLoadOrder(*m_DirectoryStructure); } void OrganizerCore::prepareStart() { if (m_CurrentProfile == nullptr) { return; } m_CurrentProfile->writeModlist(); m_CurrentProfile->createTweakedIniFile(); saveCurrentLists(); m_Settings.setupLoadMechanism(); storeSettings(); } std::vector OrganizerCore::fileMapping(const QString &profileName, const QString &customOverwrite) { // need to wait until directory structure while (m_DirectoryUpdate) { ::Sleep(100); QCoreApplication::processEvents(); } IPluginGame *game = qApp->property("managed_game").value(); Profile profile(QDir(m_Settings.getProfileDirectory() + "/" + profileName), game); MappingType result; QString dataPath = QDir::toNativeSeparators(game->dataDirectory().absolutePath()); bool overwriteActive = false; for (auto mod : profile.getActiveMods()) { if (std::get<0>(mod).compare("overwrite", Qt::CaseInsensitive) == 0) { continue; } unsigned int modIndex = ModInfo::getIndex(std::get<0>(mod)); ModInfo::Ptr modPtr = ModInfo::getByIndex(modIndex); bool createTarget = customOverwrite == std::get<0>(mod); overwriteActive |= createTarget; if (modPtr->isRegular()) { result.insert(result.end(), {QDir::toNativeSeparators(std::get<1>(mod)), dataPath, true, createTarget}); } } if (!overwriteActive && !customOverwrite.isEmpty()) { throw MyException(tr("The designated write target \"%1\" is not enabled.") .arg(customOverwrite)); } if (m_CurrentProfile->localSavesEnabled()) { LocalSavegames *localSaves = game->feature(); if (localSaves != nullptr) { MappingType saveMap = localSaves->mappings(currentProfile()->absolutePath() + "/saves"); result.reserve(result.size() + saveMap.size()); result.insert(result.end(), saveMap.begin(), saveMap.end()); } else { qWarning("local save games not supported by this game plugin"); } } result.insert(result.end(), { QDir::toNativeSeparators(m_Settings.getOverwriteDirectory()), dataPath, true, customOverwrite.isEmpty() }); for (MOBase::IPluginFileMapper *mapper : m_PluginContainer->plugins()) { IPlugin *plugin = dynamic_cast(mapper); if (plugin->isActive()) { MappingType pluginMap = mapper->mappings(); result.reserve(result.size() + pluginMap.size()); result.insert(result.end(), pluginMap.begin(), pluginMap.end()); } } return result; } std::vector OrganizerCore::fileMapping( const QString &dataPath, const QString &relPath, const DirectoryEntry *base, const DirectoryEntry *directoryEntry, int createDestination) { std::vector result; for (FileEntry::Ptr current : directoryEntry->getFiles()) { bool isArchive = false; int origin = current->getOrigin(isArchive); if (isArchive || (origin == 0)) { continue; } QString originPath = QString::fromStdWString(base->getOriginByID(origin).getPath()); QString fileName = QString::fromStdWString(current->getName()); // QString fileName = ToQString(current->getName()); QString source = originPath + relPath + fileName; QString target = dataPath + relPath + fileName; if (source != target) { result.push_back({source, target, false, false}); } } // recurse into subdirectories std::vector::const_iterator current, end; directoryEntry->getSubDirectories(current, end); for (; current != end; ++current) { int origin = (*current)->anyOrigin(); QString originPath = QString::fromStdWString(base->getOriginByID(origin).getPath()); QString dirName = QString::fromStdWString((*current)->getName()); QString source = originPath + relPath + dirName; QString target = dataPath + relPath + dirName; bool writeDestination = (base == directoryEntry) && (origin == createDestination); result.push_back({source, target, true, writeDestination}); std::vector subRes = fileMapping( dataPath, relPath + dirName + "\\", base, *current, createDestination); result.insert(result.end(), subRes.begin(), subRes.end()); } return result; }