/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "moapplication.h" #include "commandline.h" #include "instancemanager.h" #include "loglist.h" #include "mainwindow.h" #include "messagedialog.h" #include "multiprocess.h" #include "nexusinterface.h" #include "nxmaccessmanager.h" #include "organizercore.h" #include "sanitychecks.h" #include "settings.h" #include "shared/appconfig.h" #include "shared/util.h" #include "thread_utils.h" #include "tutorialmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // see addDllsToPath() below #pragma comment(linker, "/manifestDependency:\"" \ "name='dlls' " \ "processorArchitecture='x86' " \ "version='1.0.0.0' " \ "type='win32' \"") using namespace MOBase; using namespace MOShared; // style proxy that changes the appearance of drop indicators // class ProxyStyle : public QProxyStyle { public: ProxyStyle(QStyle* baseStyle = 0) : QProxyStyle(baseStyle) {} void drawPrimitive(PrimitiveElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override { if (element == QStyle::PE_IndicatorItemViewItemDrop) { // 0. Fix a bug that made the drop indicator sometimes appear on top // of the mod list when selecting a mod. if (option->rect.height() == 0 && option->rect.bottomRight() == QPoint(-1, -1)) { return; } // 1. full-width drop indicator QRect rect(option->rect); if (auto* view = qobject_cast(widget)) { rect.setLeft(view->indentation()); rect.setRight(widget->width()); } // 2. stylish drop indicator painter->setRenderHint(QPainter::Antialiasing, true); QColor col(option->palette.windowText().color()); QPen pen(col); pen.setWidth(2); col.setAlpha(50); painter->setPen(pen); painter->setBrush(QBrush(col)); if (rect.height() == 0) { QPoint tri[3] = {rect.topLeft(), rect.topLeft() + QPoint(-5, 5), rect.topLeft() + QPoint(-5, -5)}; painter->drawPolygon(tri, 3); painter->drawLine(rect.topLeft(), rect.topRight()); } else { painter->drawRoundedRect(rect, 5, 5); } } else { QProxyStyle::drawPrimitive(element, option, painter, widget); } } }; // This adds the `dlls` directory to the path so the dlls can be found. How // MO is able to find dlls in there is a bit convoluted: // // Dependencies on DLLs can be baked into an executable by passing a // `manifestdependency` option to the linker. This can be done on the command // line or with a pragma. Typically, the dependency will not be a hardcoded // filename, but an assembly name, such as Microsoft.Windows.Common-Controls. // // When Windows loads the exe, it will look for this assembly in a variety of // places, such as in the WinSxS folder, but also in the program's folder. It // will look for `assemblyname.dll` or `assemblyname/assemblyname.dll` and try // to load that. // // If these files don't exist, then the loader gets creative and looks for // `assemblyname.manifest` and `assemblyname/assemblyname.manifest`. A manifest // file is just an XML file that can contain a list of DLLs to load for this // assembly. // // In MO's case, there's a `pragma` at the beginning of this file which adds // `dlls` as an "assembly" dependency. This is a bit of a hack to just force // the loader to eventually find `dlls/dlls.manifest`, which contains the list // of all the DLLs MO requires to load. // // This file was handwritten in `modorganizer/src/dlls.manifest.qt5` and // is copied and renamed in CMakeLists.txt into `bin/dlls/dlls.manifest`. Note // that the useless and incorrect .qt5 extension is removed. // void addDllsToPath() { const auto dllsPath = QDir::toNativeSeparators(QCoreApplication::applicationDirPath() + "/dlls"); QCoreApplication::setLibraryPaths(QStringList(dllsPath) + QCoreApplication::libraryPaths()); env::prependToPath(dllsPath); } MOApplication::MOApplication(int& argc, char** argv) : QApplication(argc, argv) { TimeThis tt("MOApplication()"); qputenv("QML_DISABLE_DISK_CACHE", "true"); connect(&m_styleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file) { log::debug("style file '{}' changed, reloading", file); updateStyle(file); }); m_defaultStyle = "windowsvista"; updateStyle(m_defaultStyle); addDllsToPath(); // When MO2 is launched by nxmhandler, CWD is set to `C:\Windows\System32`. // QtWebEngineProcess crashes if CWD is not set to the application directory. QDir::setCurrent(QCoreApplication::applicationDirPath()); } OrganizerCore& MOApplication::core() { return *m_core; } void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess) { connect( &multiProcess, &MOMultiProcess::messageSent, this, [this](auto&& s) { externalMessage(s); }, Qt::QueuedConnection); } int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) { TimeThis tt("MOApplication setup()"); // makes plugin data path available to plugins, see // IOrganizer::getPluginDataPath() MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath()); // figuring out the current instance m_instance = getCurrentInstance(forceSelect); if (!m_instance) { return 1; } // first time the data path is available, set the global property and log // directory, then log a bunch of debug stuff const QString dataPath = m_instance->directory(); setProperty("dataPath", dataPath); if (!setLogDirectory(dataPath)) { reportError(tr("Failed to create log folder.")); InstanceManager::singleton().clearCurrentInstance(); return 1; } log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); log::info("starting Mod Organizer version {} revision {} in {}, usvfs: {}", createVersionInfo().string(), GITID, QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString()); if (multiProcess.secondary()) { log::debug("another instance of MO is running but --multiple was given"); } log::info("data path: {}", m_instance->directory()); log::info("working directory: {}", QDir::currentPath()); tt.start("MOApplication::doOneRun() settings"); // deleting old files, only for the main instance if (!multiProcess.secondary()) { purgeOldFiles(); } // loading settings m_settings.reset(new Settings(m_instance->iniPath(), true)); log::getDefault().setLevel(m_settings->diagnostics().logLevel()); log::debug("using ini at '{}'", m_settings->filename()); OrganizerCore::setGlobalCoreDumpType(m_settings->diagnostics().coreDumpType()); tt.start("MOApplication::doOneRun() log and checks"); // logging and checking env::Environment env; env.dump(*m_settings); m_settings->dump(); sanity::checkEnvironment(env); m_modules = std::move(env.onModuleLoaded(qApp, [](auto&& m) { if (m.interesting()) { log::debug("loaded module {}", m.toString()); } sanity::checkIncompatibleModule(m); })); auto sslBuildVersion = QSslSocket::sslLibraryBuildVersionString(); auto sslVersion = QSslSocket::sslLibraryVersionString(); log::debug("SSL Build Version: {}, SSL Runtime Version {}", sslBuildVersion, sslVersion); // nexus interface tt.start("MOApplication::doOneRun() NexusInterface"); log::debug("initializing nexus interface"); m_nexus.reset(new NexusInterface(m_settings.get())); // organizer core tt.start("MOApplication::doOneRun() OrganizerCore"); log::debug("initializing core"); m_core.reset(new OrganizerCore(*m_settings)); if (!m_core->bootstrap()) { reportError(tr("Failed to set up data paths.")); InstanceManager::singleton().clearCurrentInstance(); return 1; } // plugins tt.start("MOApplication::doOneRun() plugins"); log::debug("initializing plugins"); m_plugins = std::make_unique(m_core.get()); m_plugins->loadPlugins(); // instance if (auto r = setupInstanceLoop(*m_instance, *m_plugins)) { return *r; } if (m_instance->isPortable()) { log::debug("this is a portable instance"); } tt.start("MOApplication::doOneRun() OrganizerCore setup"); sanity::checkPaths(*m_instance->gamePlugin(), *m_settings); // setting up organizer core m_core->setManagedGame(m_instance->gamePlugin()); m_core->createDefaultProfile(); m_core->createOverwriteDirectories(); log::info("using game plugin '{}' ('{}', variant {}, steam id '{}') at {}", m_instance->gamePlugin()->gameName(), m_instance->gamePlugin()->gameShortName(), (m_settings->game().edition().value_or("").isEmpty() ? "(none)" : *m_settings->game().edition()), m_instance->gamePlugin()->steamAPPId(), m_instance->gamePlugin()->gameDirectory().absolutePath()); CategoryFactory::instance().loadCategories(); m_core->updateExecutablesList(); m_core->updateModInfoFromDisc(); m_core->setCurrentProfile(m_instance->profileName()); return 0; } int MOApplication::run(MOMultiProcess& multiProcess) { // checking command line TimeThis tt("MOApplication::run()"); // show splash tt.start("MOApplication::doOneRun() splash"); MOSplash splash(*m_settings, m_instance->directory(), m_instance->gamePlugin()); tt.start("MOApplication::doOneRun() finishing"); // start an api check NexusOAuthTokens tokens; if (GlobalSettings::nexusOAuthTokens(tokens) || GlobalSettings::nexusApiKey(tokens.apiKey)) { m_nexus->getAccessManager()->apiCheck(tokens); } // tutorials log::debug("initializing tutorials"); TutorialManager::init(qApp->applicationDirPath() + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", m_core.get()); // styling if (!setStyleFile(m_settings->interface().styleName().value_or(""))) { // disable invalid stylesheet m_settings->interface().setStyleName(""); } int res = 1; { tt.start("MOApplication::doOneRun() MainWindow setup"); MainWindow mainWindow(*m_settings, *m_core, *m_plugins); // the nexus interface can show dialogs, make sure they're parented to the // main window m_nexus->getAccessManager()->setTopLevelWidget(&mainWindow); connect( &mainWindow, &MainWindow::styleChanged, this, [this](auto&& file) { setStyleFile(file); }, Qt::QueuedConnection); log::debug("displaying main window"); mainWindow.show(); mainWindow.activateWindow(); splash.close(); tt.stop(); res = exec(); mainWindow.close(); // main window is about to be destroyed m_nexus->getAccessManager()->setTopLevelWidget(nullptr); } // reset geometry if the flag was set from the settings dialog m_settings->geometry().resetIfNeeded(); return res; } void MOApplication::externalMessage(const QString& message) { log::debug("received external message '{}'", message); MOShortcut moshortcut(message); if (moshortcut.isValid()) { if (moshortcut.hasExecutable()) { try { m_core->processRunner() .setFromShortcut(moshortcut) .setWaitForCompletion(ProcessRunner::TriggerRefresh) .run(); } catch (std::exception&) { // user was already warned } } } else if (isNxmLink(message)) { if (m_core == nullptr) { // This can happen if MO2 is started with the --pick option and no instance has // been selected yet. reportError(tr("You need to select an instance before trying to download mods.")); } else { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false); m_core->downloadRequestedNXM(message); } } else { cl::CommandLine cl; if (auto r = cl.process(message.toStdWString())) { log::debug("while processing external message, command line wants to " "exit; ignoring"); return; } if (auto i = cl.instance()) { const auto ci = InstanceManager::singleton().currentInstance(); if (*i != ci->displayName()) { reportError( tr("This shortcut or command line is for instance '%1', but the current " "instance is '%2'.") .arg(*i) .arg(ci->displayName())); return; } } if (auto p = cl.profile()) { if (*p != m_core->profileName()) { reportError( tr("This shortcut or command line is for profile '%1', but the current " "profile is '%2'.") .arg(*p) .arg(m_core->profileName())); return; } } cl.runPostOrganizer(*m_core); } } std::shared_ptr MOApplication::getCurrentInstance(bool forceSelect) { auto& m = InstanceManager::singleton(); auto currentInstance = m.currentInstance(); if (forceSelect || !currentInstance) { // clear any overrides that might have been given on the command line m.clearOverrides(); currentInstance = selectInstance(); } else { if (!QDir(currentInstance->directory()).exists()) { // the previously used instance doesn't exist anymore // clear any overrides that might have been given on the command line m.clearOverrides(); if (m.hasAnyInstances()) { reportError(QObject::tr("Instance at '%1' not found. Select another instance.") .arg(currentInstance->directory())); } else { reportError( QObject::tr("Instance at '%1' not found. You must create a new instance") .arg(currentInstance->directory())); } currentInstance = selectInstance(); } } return currentInstance; } std::optional MOApplication::setupInstanceLoop(Instance& currentInstance, PluginContainer& pc) { for (;;) { const auto setupResult = setupInstance(currentInstance, pc); if (setupResult == SetupInstanceResults::Okay) { return {}; } else if (setupResult == SetupInstanceResults::TryAgain) { continue; } else if (setupResult == SetupInstanceResults::SelectAnother) { InstanceManager::singleton().clearCurrentInstance(); return ReselectExitCode; } else { return 1; } } } void MOApplication::purgeOldFiles() { // remove the temporary backup directory in case we're restarting after an // update QString backupDirectory = qApp->applicationDirPath() + "/update_backup"; if (QDir(backupDirectory).exists()) { shellDelete(QStringList(backupDirectory)); } // cycle log file removeOldFiles(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()), "usvfs*.log", 5, QDir::Name); } void MOApplication::resetForRestart() { LogModel::instance().clear(); ResetExitFlag(); // make sure the log file isn't locked in case MO was restarted and // the previous instance gets deleted log::getDefault().setFile({}); // clear instance and profile overrides InstanceManager::singleton().clearOverrides(); m_core = {}; m_plugins = {}; m_nexus = {}; m_settings = {}; m_instance = {}; } bool MOApplication::setStyleFile(const QString& styleName) { // remove all files from watch QStringList currentWatch = m_styleWatcher.files(); if (currentWatch.count() != 0) { m_styleWatcher.removePaths(currentWatch); } // set new stylesheet or clear it if (styleName.length() != 0) { QString styleSheetName = applicationDirPath() + "/" + MOBase::ToQString(AppConfig::stylesheetsPath()) + "/" + styleName; if (QFile::exists(styleSheetName)) { m_styleWatcher.addPath(styleSheetName); updateStyle(styleSheetName); } else { updateStyle(styleName); } } else { setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle))); setStyleSheet(""); } return true; } bool MOApplication::notify(QObject* receiver, QEvent* event) { try { return QApplication::notify(receiver, event); } catch (const std::exception& e) { log::error("uncaught exception in handler (object {}, eventtype {}): {}", receiver->objectName(), event->type(), e.what()); reportError(tr("an error occurred: %1").arg(e.what())); return false; } catch (...) { log::error("uncaught non-std exception in handler (object {}, eventtype {})", receiver->objectName(), event->type()); reportError(tr("an error occurred")); return false; } } namespace { QStringList extractTopStyleSheetComments(QFile& stylesheet) { if (!stylesheet.open(QFile::ReadOnly)) { log::error("failed to open stylesheet file {}", stylesheet.fileName()); return {}; } ON_BLOCK_EXIT([&stylesheet]() { stylesheet.close(); }); QStringList topComments; while (true) { const auto byteLine = stylesheet.readLine(); if (byteLine.isNull()) { break; } const auto line = QString(byteLine).trimmed(); // skip empty lines if (line.isEmpty()) { continue; } // only handle single line comments if (!line.startsWith("/*")) { break; } topComments.push_back(line.mid(2, line.size() - 4).trimmed()); } return topComments; } QString extractBaseStyleFromStyleSheet(QFile& stylesheet, const QString& defaultStyle) { // read the first line of the files that are either empty or comments // const auto topLines = extractTopStyleSheetComments(stylesheet); const auto factoryStyles = QStyleFactory::keys(); QString style = defaultStyle; for (const auto& line : topLines) { if (!line.startsWith("mo2-base-style")) { continue; } const auto parts = line.split(":"); if (parts.size() != 2) { log::warn("found invalid top-comment for mo2 in {}: {}", stylesheet.fileName(), line); continue; } const auto tmpStyle = parts[1].trimmed(); const auto index = factoryStyles.indexOf(tmpStyle, 0, Qt::CaseInsensitive); if (index == -1) { log::warn("base style '{}' from style '{}' not found", tmpStyle, stylesheet.fileName(), line); continue; } style = factoryStyles[index]; log::info("found base style '{}' for style '{}'", style, stylesheet.fileName()); break; } return style; } } // namespace void MOApplication::updateStyle(const QString& fileName) { if (QStyleFactory::keys().contains(fileName)) { setStyleSheet(""); setStyle(new ProxyStyle(QStyleFactory::create(fileName))); } else { QFile stylesheet(fileName); if (stylesheet.exists()) { setStyle(new ProxyStyle(QStyleFactory::create( extractBaseStyleFromStyleSheet(stylesheet, m_defaultStyle)))); setStyleSheet(QString("file:///%1").arg(fileName)); } else { log::warn("invalid stylesheet: {}", fileName); } } } MOSplash::MOSplash(const Settings& settings, const QString& dataPath, const MOBase::IPluginGame* game) { const auto splashPath = getSplashPath(settings, dataPath, game); if (splashPath.isEmpty()) { return; } QPixmap image(splashPath); if (image.isNull()) { log::error("failed to load splash from {}", splashPath); return; } ss_.reset(new QSplashScreen(image)); settings.geometry().centerOnMainWindowMonitor(ss_.get()); ss_->show(); ss_->activateWindow(); } void MOSplash::close() { if (ss_) { // don't pass mainwindow as it just waits half a second for it // instead of proceding ss_->finish(nullptr); } } QString MOSplash::getSplashPath(const Settings& settings, const QString& dataPath, const MOBase::IPluginGame* game) const { if (!settings.useSplash()) { return {}; } // try splash from instance directory const QString splashPath = dataPath + "/splash.png"; if (QFile::exists(dataPath + "/splash.png")) { QImage image(splashPath); if (!image.isNull()) { return splashPath; } } // try splash from plugin QString pluginSplash = QString(":/%1/splash").arg(game->gameShortName()); if (QFile::exists(pluginSplash)) { QImage image(pluginSplash); if (!image.isNull()) { image.save(splashPath); return pluginSplash; } } // try default splash from resource QString defaultSplash = ":/MO/gui/splash"; if (QFile::exists(defaultSplash)) { QImage image(defaultSplash); if (!image.isNull()) { return defaultSplash; } } return splashPath; }