mirror of
https://github.com/ModOrganizer2/modorganizer.git
synced 2026-07-27 13:58:24 -07:00
665 lines
19 KiB
C++
665 lines
19 KiB
C++
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "moapplication.h"
|
|
#include "settings.h"
|
|
#include "env.h"
|
|
#include "commandline.h"
|
|
#include "instancemanager.h"
|
|
#include "organizercore.h"
|
|
#include "thread_utils.h"
|
|
#include "loglist.h"
|
|
#include "multiprocess.h"
|
|
#include "nexusinterface.h"
|
|
#include "nxmaccessmanager.h"
|
|
#include "tutorialmanager.h"
|
|
#include "sanitychecks.h"
|
|
#include "mainwindow.h"
|
|
#include "messagedialog.h"
|
|
#include "shared/util.h"
|
|
#include <iplugingame.h>
|
|
#include <report.h>
|
|
#include <utility.h>
|
|
#include <log.h>
|
|
#include "shared/appconfig.h"
|
|
#include <QFile>
|
|
#include <QStringList>
|
|
#include <QProxyStyle>
|
|
#include <QStyleFactory>
|
|
#include <QPainter>
|
|
#include <QStyleOption>
|
|
#include <QDebug>
|
|
|
|
// 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<const QTreeView*>(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 = style()->objectName();
|
|
setStyle(new ProxyStyle(style()));
|
|
addDllsToPath();
|
|
}
|
|
|
|
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().displayString(3), 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();
|
|
}
|
|
|
|
QWindowsWindowFunctions::setWindowActivationBehavior(
|
|
QWindowsWindowFunctions::AlwaysActivateWindow);
|
|
|
|
|
|
// 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);
|
|
}));
|
|
|
|
|
|
// 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<PluginContainer>(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();
|
|
|
|
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
|
|
QString apiKey;
|
|
if (GlobalSettings::nexusApiKey(apiKey)) {
|
|
m_nexus->getAccessManager()->apiCheck(apiKey);
|
|
}
|
|
|
|
// 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)) {
|
|
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::unique_ptr<Instance> 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<int> 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;
|
|
}
|
|
}
|
|
|
|
void MOApplication::updateStyle(const QString& fileName)
|
|
{
|
|
if (QStyleFactory::keys().contains(fileName)) {
|
|
setStyleSheet("");
|
|
setStyle(new ProxyStyle(QStyleFactory::create(fileName)));
|
|
} else {
|
|
setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle)));
|
|
if (QFile::exists(fileName)) {
|
|
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;
|
|
}
|