summaryrefslogtreecommitdiff
path: root/src/main.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.cpp')
-rw-r--r--src/main.cpp1097
1 files changed, 438 insertions, 659 deletions
diff --git a/src/main.cpp b/src/main.cpp
index 6b1ec482..bfd23103 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -17,136 +17,85 @@ You should have received a copy of the GNU General Public License
along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifdef LEAK_CHECK_WITH_VLD
-#include <wchar.h>
-#include <vld.h>
-#endif // LEAK_CHECK_WITH_VLD
-
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <DbgHelp.h>
-
-#include "shared/appconfig.h"
-#include <utility.h>
-#include <scopeguard.h>
#include "mainwindow.h"
-#include <report.h>
-#include "modlist.h"
-#include "profile.h"
-#include "spawn.h"
-#include "executableslist.h"
#include "singleinstance.h"
-#include "utility.h"
#include "loglist.h"
#include "selectiondialog.h"
#include "moapplication.h"
#include "tutorialmanager.h"
#include "nxmaccessmanager.h"
#include "instancemanager.h"
-#include "moshortcut.h"
+#include "instancemanagerdialog.h"
+#include "createinstancedialog.h"
+#include "createinstancedialogpages.h"
#include "organizercore.h"
#include "env.h"
#include "envmodule.h"
+#include "commandline.h"
#include "shared/util.h"
+#include "shared/appconfig.h"
-#include <eh.h>
-#include "shared/windows_error.h"
+#include <imoinfo.h>
+#include <report.h>
#include <usvfs.h>
#include <log.h>
+#include <utility.h>
-#include <QApplication>
-#include <QPushButton>
-#include <QListWidget>
-#include <QComboBox>
-#include <QCheckBox>
-#include <QDir>
-#include <QFileInfo>
-#include <QWhatsThis>
-#include <QToolBar>
-#include <QFileDialog>
-#include <QDesktopServices>
-#include <QMessageBox>
-#include <QSharedMemory>
-#include <QBuffer>
-#include <QSplashScreen>
-#include <QDirIterator>
-#include <QDesktopServices>
-#include <QLibraryInfo>
-#include <QSslSocket>
-#include <QtPlatformHeaders/QWindowsWindowFunctions>
-
-#include <boost/scoped_array.hpp>
-
-#include <ShellAPI.h>
-
-#include <cstdarg>
-#include <iostream>
-#include <sstream>
-#include <stdexcept>
-
-
-#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"")
+// see addDllsToPath() below
+#pragma comment(linker, "/manifestDependency:\"" \
+ "name='dlls' " \
+ "processorArchitecture='x86' " \
+ "version='1.0.0.0' " \
+ "type='win32' \"")
using namespace MOBase;
using namespace MOShared;
-
void sanityChecks(const env::Environment& env);
int checkIncompatibleModule(const env::Module& m);
int checkPathsForSanity(MOBase::IPluginGame& game, const Settings& s);
-bool createAndMakeWritable(const std::wstring &subPath) {
- QString const dataPath = qApp->property("dataPath").toString();
- QString fullPath = dataPath + "/" + QString::fromStdWString(subPath);
-
- if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) {
- QMessageBox::critical(nullptr, QObject::tr("Error"),
- QObject::tr("Failed to create \"%1\". Your user "
- "account probably lacks permission.")
- .arg(fullPath));
- return false;
- } else {
- return true;
- }
-}
-
-bool bootstrap()
+void purgeOldFiles()
{
- // remove the temporary backup directory in case we're restarting after an update
+ // 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);
-
- return true;
+ removeOldFiles(
+ qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()),
+ "usvfs*.log", 5, QDir::Name);
}
-thread_local LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr;
-thread_local std::terminate_handler prevTerminateHandler = nullptr;
+thread_local LPTOP_LEVEL_EXCEPTION_FILTER g_prevExceptionFilter = nullptr;
+thread_local std::terminate_handler g_prevTerminateHandler = nullptr;
-LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs)
+LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs)
{
const std::wstring& dumpPath = OrganizerCore::crashDumpsPath();
- int dumpRes =
- CreateMiniDump(exceptionPtrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str());
- if (!dumpRes)
+
+ const int r = CreateMiniDump(
+ ptrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str());
+
+ if (r == 0) {
log::error("ModOrganizer has crashed, crash dump created.");
- else
- log::error("ModOrganizer has crashed, CreateMiniDump failed ({}, error {}).", dumpRes, GetLastError());
+ } else {
+ log::error(
+ "ModOrganizer has crashed, CreateMiniDump failed ({}, error {}).",
+ r, GetLastError());
+ }
- if (prevUnhandledExceptionFilter && exceptionPtrs)
- return prevUnhandledExceptionFilter(exceptionPtrs);
+ if (g_prevExceptionFilter && ptrs)
+ return g_prevExceptionFilter(ptrs);
else
return EXCEPTION_CONTINUE_SEARCH;
}
-void terminateHandler() noexcept
+void onTerminate() noexcept
{
__try
{
@@ -155,361 +104,383 @@ void terminateHandler() noexcept
}
__except
(
- MyUnhandledExceptionFilter(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER
+ onUnhandledException(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER
)
{
}
- if (prevTerminateHandler) {
- prevTerminateHandler();
+ if (g_prevTerminateHandler) {
+ g_prevTerminateHandler();
} else {
std::abort();
}
}
-void setUnhandledExceptionHandler()
+void setExceptionHandlers()
{
- prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
- prevTerminateHandler = std::set_terminate(terminateHandler);
+ g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException);
+ g_prevTerminateHandler = std::set_terminate(onTerminate);
}
-// Parses the first parseArgCount arguments of the current process command line and returns
-// them in parsedArgs, the rest of the command line is returned untouched.
-LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector<std::wstring>& parsedArgs)
+// 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()
{
- LPCWSTR cmd = GetCommandLineW();
- LPCWSTR arg = nullptr; // to skip executable name
- for (; parseArgCount >= 0 && *cmd; ++cmd)
- {
- if (*cmd == '"') {
- int escaped = 0;
- for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd)
- escaped = *cmd == '\\' ? escaped + 1 : 0;
- }
- if (*cmd == ' ') {
- if (arg)
- if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"')
- parsedArgs.push_back(std::wstring(arg+1, cmd-1));
- else
- parsedArgs.push_back(std::wstring(arg, cmd));
- arg = cmd + 1;
- --parseArgCount;
- }
- }
- return cmd;
-}
+ const auto dllsPath = QDir::toNativeSeparators(
+ QCoreApplication::applicationDirPath() + "/dlls");
+ QCoreApplication::setLibraryPaths(
+ QStringList(dllsPath) + QCoreApplication::libraryPaths());
-static int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) {
- PROCESS_INFORMATION pi{ 0 };
- STARTUPINFO si{ 0 };
- si.cb = sizeof(si);
- std::wstring commandLineCopy = commandLine;
+ env::prependToPath(dllsPath);
+}
- if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) {
- // A bit of a problem where to log the error message here, at least this way you can get the message
- // using a either DebugView or a live debugger:
- std::wostringstream ost;
- ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError();
- OutputDebugStringW(ost.str().c_str());
- return -1;
+QString getSplashPath(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game)
+{
+ if (!settings.useSplash()) {
+ return {};
}
- WaitForSingleObject(pi.hProcess, INFINITE);
+ // 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;
+ }
+ }
- DWORD exitCode = (DWORD)-1;
- ::GetExitCodeProcess(pi.hProcess, &exitCode);
- CloseHandle(pi.hThread);
- CloseHandle(pi.hProcess);
- return static_cast<int>(exitCode);
-}
+ // 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;
+ }
+ }
-static DWORD WaitForProcess() {
+ // try default splash from resource
+ QString defaultSplash = ":/MO/gui/splash";
+ if (QFile::exists(defaultSplash)) {
+ QImage image(defaultSplash);
+ if (!image.isNull()) {
+ return defaultSplash;
+ }
+ }
+ return splashPath;
}
-static bool HaveWriteAccess(const std::wstring &path)
+std::unique_ptr<QSplashScreen> createSplash(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game)
{
- bool writable = false;
+ 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 {};
+ }
- const static SECURITY_INFORMATION requestedFileInformation = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
+ auto splash = std::make_unique<QSplashScreen>(image);
+ settings.geometry().centerOnMainWindowMonitor(splash.get());
- DWORD length = 0;
- if (!::GetFileSecurityW(path.c_str(), requestedFileInformation, nullptr, 0UL, &length)
- && (::GetLastError() == ERROR_INSUFFICIENT_BUFFER)) {
- std::string tempBuffer;
- tempBuffer.reserve(length);
- PSECURITY_DESCRIPTOR security = (PSECURITY_DESCRIPTOR)tempBuffer.data();
- if (security
- && ::GetFileSecurity(path.c_str(), requestedFileInformation, security, length, &length)) {
- HANDLE token = nullptr;
- const static DWORD tokenDesiredAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ;
- if (!::OpenThreadToken(::GetCurrentThread(), tokenDesiredAccess, TRUE, &token)) {
- if (!::OpenProcessToken(::GetCurrentProcess(), tokenDesiredAccess, &token)) {
- throw std::runtime_error("Unable to get any thread or process token");
- }
- }
+ splash->show();
+ splash->activateWindow();
- HANDLE impersonatedToken = nullptr;
- if (::DuplicateToken(token, SecurityImpersonation, &impersonatedToken)) {
- GENERIC_MAPPING mapping = { 0xFFFFFFFF };
- mapping.GenericRead = FILE_GENERIC_READ;
- mapping.GenericWrite = FILE_GENERIC_WRITE;
- mapping.GenericExecute = FILE_GENERIC_EXECUTE;
- mapping.GenericAll = FILE_ALL_ACCESS;
+ return splash;
+}
- DWORD genericAccessRights = FILE_GENERIC_WRITE;
- ::MapGenericMask(&genericAccessRights, &mapping);
+std::optional<int> handleCommandLine(
+ const cl::CommandLine& cl, OrganizerCore& organizer)
+{
+ // if we have a command line parameter, it is either a nxm link or
+ // a binary to start
+ if (cl.shortcut().isValid()) {
+ if (cl.shortcut().hasExecutable()) {
+ try {
+ organizer.processRunner()
+ .setFromShortcut(cl.shortcut())
+ .setWaitForCompletion()
+ .run();
- PRIVILEGE_SET privileges = { 0 };
- DWORD grantedAccess = 0;
- DWORD privilegesLength = sizeof(privileges);
- BOOL result = 0;
- if (::AccessCheck(security, impersonatedToken, genericAccessRights, &mapping, &privileges, &privilegesLength, &grantedAccess, &result)) {
- writable = result != 0;
- }
- ::CloseHandle(impersonatedToken);
+ return 0;
+ }
+ catch (const std::exception &e) {
+ reportError(
+ QObject::tr("failed to start shortcut: %1").arg(e.what()));
+ return 1;
}
+ }
+ } else if (cl.nxmLink()) {
+ log::debug("starting download from command line: {}", *cl.nxmLink());
+ organizer.externalMessage(*cl.nxmLink());
+ } else if (cl.executable()) {
+ const QString exeName = *cl.executable();
+ log::debug("starting {} from command line", exeName);
- ::CloseHandle(token);
+ try
+ {
+ // pass the remaining parameters to the binary
+ organizer.processRunner()
+ .setFromFileOrExecutable(exeName, cl.untouched())
+ .setWaitForCompletion()
+ .run();
+
+ return 0;
+ }
+ catch (const std::exception &e)
+ {
+ reportError(
+ QObject::tr("failed to start application: %1").arg(e.what()));
+ return 1;
}
}
- return writable;
-}
+ return {};
+}
-QString determineProfile(QStringList &arguments, const Settings &settings)
+std::optional<Instance> selectInstance()
{
- auto selectedProfileName = settings.game().selectedProfileName();
+ auto& m = InstanceManager::singleton();
+
+ NexusInterface ni(nullptr);
+ PluginContainer pc(nullptr);
+ pc.loadPlugins();
- { // see if there is a profile on the command line
- int profileIndex = arguments.indexOf("-p", 1);
- if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) {
- log::debug("profile overwritten on command line");
- selectedProfileName = arguments.at(profileIndex + 1);
+ if (m.instancePaths().empty() && !m.portableInstanceExists()) {
+ // no instances configured
+ CreateInstanceDialog dlg(pc, nullptr);
+ if (dlg.exec() != QDialog::Accepted) {
+ return {};
}
- arguments.removeAt(profileIndex);
- arguments.removeAt(profileIndex);
- }
- if (!selectedProfileName) {
- log::debug("no configured profile");
- selectedProfileName = "Default";
+ return m.currentInstance();
}
- return *selectedProfileName;
-}
-MOBase::IPluginGame *selectGame(
- Settings &settings, QDir const &gamePath, MOBase::IPluginGame *game)
-{
- settings.game().setName(game->gameName());
+ InstanceManagerDialog dlg(pc);
+ dlg.setRestartOnSelect(false);
- QString gameDir = gamePath.absolutePath();
- game->setGamePath(gameDir);
+ dlg.show();
+ dlg.activateWindow();
+ dlg.raise();
- settings.game().setDirectory(gameDir);
+ if (dlg.exec() != QDialog::Accepted) {
+ return {};
+ }
- return game;
+ return m.currentInstance();
}
+enum class SetupInstanceResults
+{
+ Ok,
+ TryAgain,
+ SelectAnother,
+ Exit
+};
+
-MOBase::IPluginGame *determineCurrentGame(
- QString const &moPath, Settings &settings, PluginContainer const &plugins)
+void criticalOnTop(const QString& message)
{
- //Determine what game we are running where. Be very paranoid in case the
- //user has done something odd.
+ QMessageBox mb(QMessageBox::Critical, QObject::tr("Mod Organizer"), message);
- //If the game name has been set up, try to use that.
- const auto gameName = settings.game().name();
- const bool gameConfigured = (gameName.has_value() && *gameName != "");
+ mb.show();
+ mb.activateWindow();
+ mb.raise();
+ mb.exec();
+}
- if (gameConfigured) {
- MOBase::IPluginGame *game = plugins.managedGame(*gameName);
- if (game == nullptr) {
- reportError(
- QObject::tr("Plugin to handle %1 no longer installed. An antivirus might have deleted files.")
- .arg(*gameName));
- return nullptr;
- }
+SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc)
+{
+ const auto setupResult = instance.setup(pc);
- auto gamePath = settings.game().directory();
- if (!gamePath || *gamePath == "") {
- gamePath = game->gameDirectory().absolutePath();
+ switch (setupResult)
+ {
+ case Instance::SetupResults::Ok:
+ {
+ return SetupInstanceResults::Ok;
}
- QDir gameDir(*gamePath);
- QFileInfo directoryInfo(gameDir.path());
+ case Instance::SetupResults::BadIni:
+ {
+ criticalOnTop(
+ QObject::tr("Cannot open instance '%1', failed to read INI file %2.")
+ .arg(instance.name()).arg(instance.iniPath()));
- if (directoryInfo.isSymLink()) {
- reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. "
- "This setup is incompatible with MO2's VFS and will not run correctly.").arg(*gamePath));
+ return SetupInstanceResults::SelectAnother;
}
- if (game->looksValid(gameDir)) {
- return selectGame(settings, gameDir, game);
- }
- }
+ case Instance::SetupResults::IniMissingGame:
+ {
+ criticalOnTop(
+ QObject::tr(
+ "Cannot open instance '%1', the managed game was not found in the INI "
+ "file %2. Select the game managed by this instance.")
+ .arg(instance.name()).arg(instance.iniPath()));
- //If we've made it this far and the instance is already configured for a game, something has gone wrong.
- //Tell the user about it.
- if (gameConfigured) {
- const auto gamePath = settings.game().directory();
+ CreateInstanceDialog dlg(pc, nullptr);
+ dlg.setSinglePage<cid::GamePage>(instance.name());
- reportError(
- QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\".")
- .arg(*gameName).arg(gamePath ? *gamePath : ""));
- }
+ dlg.show();
+ dlg.activateWindow();
+ dlg.raise();
- SelectionDialog selection(gameConfigured ?
- QObject::tr("Please select the installation of %1 to manage").arg(*gameName) :
- QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32));
+ if (dlg.exec() != QDialog::Accepted) {
+ return SetupInstanceResults::Exit;
+ }
- for (IPluginGame *game : plugins.plugins<IPluginGame>()) {
- //If a game is already configured, skip any plugins that are not for that game
- if (gameConfigured && gameName->compare(game->gameName(), Qt::CaseInsensitive) != 0)
- continue;
+ instance.setGame(
+ dlg.creationInfo().game->gameName(),
+ dlg.creationInfo().gameLocation);
- //Only add games that are installed
- if (game->isInstalled()) {
- QString path = game->gameDirectory().absolutePath();
- selection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game));
+ return SetupInstanceResults::TryAgain;
}
- }
- selection.addChoice(QString("Browse..."), QString(), QVariant::fromValue(static_cast<IPluginGame *>(nullptr)));
+ case Instance::SetupResults::PluginGone:
+ {
+ criticalOnTop(
+ QObject::tr(
+ "Cannot open instance '%1', the game plugin '%2' doesn't exist. It "
+ "may have been deleted by an antivirus. Select another instance.")
+ .arg(instance.name()).arg(instance.gameName()));
- while (selection.exec() != QDialog::Rejected) {
- IPluginGame * game = selection.getChoiceData().value<IPluginGame *>();
- QString gamePath = selection.getChoiceDescription();
- QFileInfo directoryInfo(gamePath);
- if (directoryInfo.isSymLink()) {
- reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. "
- "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath));
- }
- if (game != nullptr) {
- return selectGame(settings, game->gameDirectory(), game);
+ return SetupInstanceResults::SelectAnother;
}
- gamePath = QFileDialog::getExistingDirectory(nullptr, gameConfigured ?
- QObject::tr("Please select the installation of %1 to manage").arg(*gameName) :
- QObject::tr("Please select the game to manage"),
- QString(), QFileDialog::ShowDirsOnly);
+ case Instance::SetupResults::GameGone:
+ {
+ criticalOnTop(
+ QObject::tr(
+ "Cannot open instance '%1', the game directory '%2' doesn't exist or "
+ "the game plugin '%3' doesn't recognize it. Select the game managed "
+ "by this instance.")
+ .arg(instance.name())
+ .arg(instance.gameDirectory())
+ .arg(instance.gameName()));
- if (!gamePath.isEmpty()) {
- QDir gameDir(gamePath);
- QFileInfo directoryInfo(gamePath);
- if (directoryInfo.isSymLink()) {
- reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. "
- "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath));
- }
- QList<IPluginGame *> possibleGames;
- for (IPluginGame * const game : plugins.plugins<IPluginGame>()) {
- //If a game is already configured, skip any plugins that are not for that game
- if (gameConfigured && gameName->compare(game->gameName(), Qt::CaseInsensitive) != 0)
- continue;
+ CreateInstanceDialog dlg(pc, nullptr);
+ dlg.setSinglePage<cid::GamePage>(instance.name());
+
+ dlg.show();
+ dlg.activateWindow();
+ dlg.raise();
- //Only try plugins that look valid for this directory
- if (game->looksValid(gameDir)) {
- possibleGames.append(game);
- }
+ if (dlg.exec() != QDialog::Accepted) {
+ return SetupInstanceResults::Exit;
}
- if (possibleGames.count() > 1) {
- SelectionDialog browseSelection(gameConfigured ?
- QObject::tr("Please select the installation of %1 to manage").arg(*gameName) :
- QObject::tr("Please select the game to manage"),
- nullptr, QSize(32, 32));
+ instance.setGame(
+ dlg.creationInfo().game->gameName(),
+ dlg.creationInfo().gameLocation);
- for (IPluginGame *game : possibleGames) {
- browseSelection.addChoice(game->gameIcon(), game->gameName(), gamePath, QVariant::fromValue(game));
- }
+ return SetupInstanceResults::TryAgain;
+ }
- if (browseSelection.exec() == QDialog::Accepted) {
- return selectGame(settings, gameDir, browseSelection.getChoiceData().value<IPluginGame *>());
- } else {
- reportError(gameConfigured ?
- QObject::tr("Canceled finding %1 in \"%2\".").arg(*gameName).arg(gamePath) :
- QObject::tr("Canceled finding game in \"%1\".").arg(gamePath));
- }
- } else if(possibleGames.count() == 1) {
- return selectGame(settings, gameDir, possibleGames[0]);
- } else {
- if (gameConfigured) {
- reportError(
- QObject::tr("%1 not identified in \"%2\". The directory is required to contain the game binary.")
- .arg(*gameName).arg(gamePath));
- } else {
- QString supportedGames;
+ case Instance::SetupResults::MissingVariant:
+ {
+ CreateInstanceDialog dlg(pc, nullptr);
- for (IPluginGame * const game : plugins.plugins<IPluginGame>()) {
- supportedGames += "<li>" + game->gameName() + "</li>";
- }
+ dlg.getPage<cid::GamePage>()->select(
+ instance.gamePlugin(), instance.gameDirectory());
- QString text = QObject::tr(
- "No game identified in \"%1\". The directory is required to "
- "contain the game binary.<br><br>"
- "<b>These are the games supported by Mod Organizer:</b>"
- "<ul>%2</ul>")
- .arg(gamePath)
- .arg(supportedGames);
+ dlg.setSinglePage<cid::VariantsPage>(instance.name());
- reportError(text);
- }
+ dlg.show();
+ dlg.activateWindow();
+ dlg.raise();
+
+ if (dlg.exec() != QDialog::Accepted) {
+ return SetupInstanceResults::Exit;
}
- }
- }
- return nullptr;
-}
+ instance.setVariant(dlg.creationInfo().gameVariant);
-static QString getVersionDisplayString()
-{
- return createVersionInfo().displayString(3);
-}
+ return SetupInstanceResults::TryAgain;
+ }
+ default:
+ {
+ return SetupInstanceResults::Exit;
+ }
+ }
+}
-int runApplication(MOApplication &application, SingleInstance &instance,
- const QString &splashPath)
+int runApplication(
+ MOApplication &application, const cl::CommandLine& cl,
+ SingleInstance &instance, const QString &dataPath,
+ Instance& currentInstance)
{
TimeThis tt("runApplication() to exec()");
log::info(
"starting Mod Organizer version {} revision {} in {}, usvfs: {}",
- getVersionDisplayString(), GITID, QCoreApplication::applicationDirPath(),
- MOShared::getUsvfsVersionString());
+ createVersionInfo().displayString(3), GITID,
+ QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString());
- if (!QSslSocket::supportsSsl()) {
- log::warn("no ssl support");
- }
-
- const QString dataPath = application.property("dataPath").toString();
log::info("data path: {}", dataPath);
if (InstanceManager::isPortablePath(dataPath)) {
log::debug("this is a portable instance");
}
- if (!bootstrap()) {
- reportError("failed to set up data paths");
- InstanceManager::instance().clearCurrentInstance();
- return 1;
+ log::info("working directory: {}", QDir::currentPath());
+
+ if (!instance.secondary()) {
+ purgeOldFiles();
}
QWindowsWindowFunctions::setWindowActivationBehavior(
QWindowsWindowFunctions::AlwaysActivateWindow);
- QStringList arguments = application.arguments();
-
- try {
- log::info("working directory: {}", QDir::currentPath());
+ try
+ {
+ Settings settings(
+ dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()),
+ true);
- Settings settings(dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()));
log::getDefault().setLevel(settings.diagnostics().logLevel());
log::debug("using ini at '{}'", settings.filename());
+ if (instance.secondary()) {
+ log::debug("another instance of MO is running but --multiple was given");
+ }
+
// global crashDumpType sits in OrganizerCore to make a bit less ugly to
// update it when the settings are changed during runtime
OrganizerCore::setGlobalCrashDumpsType(settings.diagnostics().crashDumpsType());
@@ -524,172 +495,70 @@ int runApplication(MOApplication &application, SingleInstance &instance,
checkIncompatibleModule(m);
});
- log::debug("initializing core");
+ // this must outlive `organizer`
std::unique_ptr<PluginContainer> pluginContainer;
+
+ log::debug("initializing nexus interface");
+ NexusInterface ni(&settings);
+
+ log::debug("initializing core");
OrganizerCore organizer(settings);
if (!organizer.bootstrap()) {
reportError("failed to set up data paths");
- InstanceManager::instance().clearCurrentInstance();
+ InstanceManager::singleton().clearCurrentInstance();
return 1;
}
- {
- // log if there are any dmp files
- const auto hasCrashDumps =
- !QDir(QString::fromStdWString(organizer.crashDumpsPath()))
- .entryList({"*.dmp"}, QDir::Files)
- .empty();
-
- if (hasCrashDumps) {
- log::debug(
- "there are crash dumps in '{}'",
- QString::fromStdWString(organizer.crashDumpsPath()));
- }
- }
-
log::debug("initializing plugins");
pluginContainer = std::make_unique<PluginContainer>(&organizer);
pluginContainer->loadPlugins();
- MOBase::IPluginGame *game = determineCurrentGame(
- application.applicationDirPath(), settings, *pluginContainer);
-
- if (game == nullptr) {
- InstanceManager &instance = InstanceManager::instance();
- QString instanceName = instance.currentInstance();
+ for (;;)
+ {
+ const auto setupResult = setupInstance(currentInstance, *pluginContainer);
- if (instanceName.compare("Portable", Qt::CaseInsensitive) != 0) {
- instance.clearCurrentInstance();
+ if (setupResult == SetupInstanceResults::Ok) {
+ break;
+ } else if (setupResult == SetupInstanceResults::TryAgain) {
+ continue;
+ } else if (setupResult == SetupInstanceResults::SelectAnother) {
+ InstanceManager::singleton().clearCurrentInstance();
return RestartExitCode;
+ } else {
+ return 1;
}
-
- return 1;
}
- checkPathsForSanity(*game, settings);
-
- bool useSplash = settings.useSplash();
+ checkPathsForSanity(*currentInstance.gamePlugin(), settings);
- if (useSplash) {
- if (splashPath.startsWith(':')) {
- // currently using MO splash, see if the plugin contains one
- QString pluginSplash
- = QString(":/%1/splash").arg(game->gameShortName());
- QImage image(pluginSplash);
- if (!image.isNull()) {
- image.save(dataPath + "/splash.png");
- }
- }
- }
-
- organizer.setManagedGame(game);
+ organizer.setManagedGame(currentInstance.gamePlugin());
organizer.createDefaultProfile();
- QString edition;
-
- if (auto v=settings.game().edition()) {
- edition = *v;
- } else {
- QStringList editions = game->gameVariants();
- if (editions.size() > 1) {
- SelectionDialog selection(
- QObject::tr("Please select the game edition you have (MO can't "
- "start the game correctly if this is set "
- "incorrectly!)"),
- nullptr);
- selection.setWindowFlag(Qt::WindowStaysOnTopHint, true);
- int index = 0;
- for (const QString &edition : editions) {
- selection.addChoice(edition, "", index++);
- }
- if (selection.exec() == QDialog::Rejected) {
- return 1;
- } else {
- edition = selection.getChoiceString();
- settings.game().setEdition(edition);
- }
- }
- }
-
- game->setGameVariant(edition);
-
log::info(
- "using game plugin '{}' ('{}', steam id '{}') at {}",
- game->gameName(), game->gameShortName(), game->steamAPPId(),
- game->gameDirectory().absolutePath());
+ "using game plugin '{}' ('{}', variant {}, steam id '{}') at {}",
+ currentInstance.gamePlugin()->gameName(),
+ currentInstance.gamePlugin()->gameShortName(),
+ (settings.game().edition().value_or("").isEmpty() ?
+ "(none)" : *settings.game().edition()),
+ currentInstance.gamePlugin()->steamAPPId(),
+ currentInstance.gamePlugin()->gameDirectory().absolutePath());
+
CategoryFactory::instance().loadCategories();
organizer.updateExecutablesList();
organizer.updateModInfoFromDisc();
- QString selectedProfileName = determineProfile(arguments, settings);
- organizer.setCurrentProfile(selectedProfileName);
-
- // if we have a command line parameter, it is either a nxm link or
- // a binary to start
- if (arguments.size() > 1) {
- if (MOShortcut shortcut{ arguments.at(1) }) {
- if (shortcut.hasExecutable()) {
- try {
- organizer.processRunner()
- .setFromShortcut(shortcut)
- .setWaitForCompletion()
- .run();
-
- return 0;
- }
- catch (const std::exception &e) {
- reportError(
- QObject::tr("failed to start shortcut: %1").arg(e.what()));
- return 1;
- }
- }
- }
- else if (OrganizerCore::isNxmLink(arguments.at(1))) {
- log::debug("starting download from command line: {}", arguments.at(1));
- organizer.externalMessage(arguments.at(1));
- }
- else {
- QString exeName = arguments.at(1);
- log::debug("starting {} from command line", exeName);
-
- arguments.removeFirst(); // remove application name (ModOrganizer.exe)
- arguments.removeFirst(); // remove binary name
-
- try
- {
- // pass the remaining parameters to the binary
- organizer.processRunner()
- .setFromFileOrExecutable(exeName, arguments)
- .setWaitForCompletion()
- .run();
- return 0;
- }
- catch (const std::exception &e)
- {
- reportError(
- QObject::tr("failed to start application: %1").arg(e.what()));
- return 1;
- }
- }
- }
+ organizer.setCurrentProfile(currentInstance.profileName());
- QPixmap pixmap;
-
- QSplashScreen splash;
-
- if (useSplash) {
- pixmap = QPixmap(splashPath);
- splash.setPixmap(pixmap);
-
- settings.geometry().centerOnMainWindowMonitor(&splash);
- splash.show();
- splash.activateWindow();
+ if (auto r=handleCommandLine(cl, organizer)) {
+ return *r;
}
+ auto splash = createSplash(settings, dataPath, currentInstance.gamePlugin());
+
QString apiKey;
- if (settings.nexus().apiKey(apiKey)) {
- NexusInterface::instance(pluginContainer.get())->getAccessManager()->apiCheck(apiKey);
+ if (GlobalSettings::nexusApiKey(apiKey)) {
+ ni.getAccessManager()->apiCheck(apiKey);
}
log::debug("initializing tutorials");
@@ -707,8 +576,7 @@ int runApplication(MOApplication &application, SingleInstance &instance,
// set up main window and its data structures
MainWindow mainWindow(settings, organizer, *pluginContainer);
- NexusInterface::instance(pluginContainer.get())
- ->getAccessManager()->setTopLevelWidget(&mainWindow);
+ ni.getAccessManager()->setTopLevelWidget(&mainWindow);
QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application,
SLOT(setStyleFile(QString)));
@@ -719,10 +587,10 @@ int runApplication(MOApplication &application, SingleInstance &instance,
mainWindow.show();
mainWindow.activateWindow();
- if (useSplash) {
+ if (splash) {
// don't pass mainwindow as it just waits half a second for it
// instead of proceding
- splash.finish(nullptr);
+ splash->finish(nullptr);
}
tt.stop();
@@ -730,237 +598,148 @@ int runApplication(MOApplication &application, SingleInstance &instance,
res = application.exec();
mainWindow.close();
- NexusInterface::instance(pluginContainer.get())
- ->getAccessManager()->setTopLevelWidget(nullptr);
+ ni.getAccessManager()->setTopLevelWidget(nullptr);
}
settings.geometry().resetIfNeeded();
return res;
- } catch (const std::exception &e) {
+ }
+ catch (const std::exception &e)
+ {
reportError(e.what());
}
return 1;
}
-int doCoreDump(env::CoreDumpTypes type)
+int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl)
{
- env::Console c;
-
- // dump
- const auto b = env::coredumpOther(type);
- if (!b) {
- std::wcerr << L"\n>>>> a minidump file was not written\n\n";
+ if (cl.shortcut().isValid()) {
+ instance.sendMessage(cl.shortcut().toString());
+ } else if (cl.nxmLink()) {
+ instance.sendMessage(*cl.nxmLink());
+ } else {
+ QMessageBox::information(
+ nullptr, QObject::tr("Mod Organizer"),
+ QObject::tr("An instance of Mod Organizer is already running"));
}
- std::wcerr << L"Press enter to continue...";
- std::wcin.get();
-
- return (b ? 0 : 1);
+ return 0;
}
-log::Levels convertQtLevel(QtMsgType t)
+void resetForRestart(cl::CommandLine& cl)
{
- switch (t)
- {
- case QtDebugMsg:
- return log::Debug;
-
- case QtWarningMsg:
- return log::Warning;
+ LogModel::instance().clear();
+ ResetExitFlag();
- case QtCriticalMsg: // fall-through
- case QtFatalMsg:
- return log::Error;
+ // make sure the log file isn't locked in case MO was restarted and
+ // the previous instance gets deleted
+ log::getDefault().setFile({});
- case QtInfoMsg: // fall-through
- default:
- return log::Info;
- }
+ // don't reprocess command line
+ cl.clear();
}
-void qtLogCallback(
- QtMsgType type, const QMessageLogContext& context, const QString& message)
+int doOneRun(
+ cl::CommandLine& cl, MOApplication& application, SingleInstance& instance)
{
- std::string_view file = "";
+ TimeThis tt("doOneRun() to runApplication()");
- if (type != QtDebugMsg) {
- if (context.file) {
- file = context.file;
+ // resets things when MO is "restarted"
+ resetForRestart(cl);
- const auto lastSep = file.find_last_of("/\\");
- if (lastSep != std::string_view::npos) {
- file = {context.file + lastSep + 1};
- }
- }
- }
+ auto& m = InstanceManager::singleton();
+ auto currentInstance = m.currentInstance();
- if (file.empty()) {
- log::log(
- convertQtLevel(type), "{}",
- message.toStdString());
- } else {
- log::log(
- convertQtLevel(type), "[{}:{}] {}",
- file, context.line, message.toStdString());
+ if (!currentInstance)
+ {
+ currentInstance = selectInstance();
+ if (!currentInstance) {
+ return 1;
+ }
}
-}
-
-void initLogging()
-{
- LogModel::create();
+ else
+ {
+ if (!currentInstance->directory().exists()) {
+ // the previously used instance doesn't exist anymore
- log::LoggerConfiguration conf;
- conf.maxLevel = MOBase::log::Debug;
- conf.pattern = "%^[%Y-%m-%d %H:%M:%S.%e %L] %v%$";
- conf.utc = true;
+ if (m.instanceNames().empty() && !m.portableInstanceExists()) {
+ criticalOnTop(QObject::tr(
+ "Instance at '%1' not found. You must create a new instance")
+ .arg(currentInstance->directory().absolutePath()));
+ } else {
+ criticalOnTop(QObject::tr(
+ "Instance at '%1' not found. Select another instance.")
+ .arg(currentInstance->directory().absolutePath()));
+ }
- log::createDefault(conf);
+ currentInstance = selectInstance();
+ if (!currentInstance) {
+ return 1;
+ }
+ }
+ }
- log::getDefault().setCallback(
- [](log::Entry e){ LogModel::instance().add(e); });
+ const QString dataPath = currentInstance->directory().path();
+ application.setProperty("dataPath", dataPath);
- qInstallMessageHandler(qtLogCallback);
-}
+ setExceptionHandlers();
-void preloadDll(const QString& filename)
-{
- if (GetModuleHandleW(filename.toStdWString().c_str()))
- return;
+ if (!setLogDirectory(dataPath)) {
+ reportError("Failed to create log folder");
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
- const auto appPath = QDir::toNativeSeparators(QDir::currentPath());
- const auto dllPath = appPath + "\\dlls\\" + filename;
+ log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
- if (!QFile::exists(dllPath)) {
- log::warn("{} not found", dllPath);
- return;
- }
+ tt.stop();
- if (!LoadLibraryW(dllPath.toStdWString().c_str())) {
- const auto e = GetLastError();
- log::warn("failed to load {}: {}", dllPath, formatSystemMessage(e));
- }
+ return runApplication(application, cl, instance, dataPath, *currentInstance);
}
-
int main(int argc, char *argv[])
{
- TimeThis tt("main to runApplication()");
-
- // handle --crashdump first
- for (int i=1; i<argc; ++i) {
- if (std::strcmp(argv[i], "--crashdump") == 0) {
- return doCoreDump(env::CoreDumpTypes::Mini);
- } else if (std::strcmp(argv[i], "--crashdump-data") == 0) {
- return doCoreDump(env::CoreDumpTypes::Data);
- } else if (std::strcmp(argv[i], "--crashdump-full") == 0) {
- return doCoreDump(env::CoreDumpTypes::Full);
- }
- }
-
- preloadDll("libcrypto-1_1-x64.dll");
- preloadDll("libssl-1_1-x64.dll");
-
- initLogging();
+ cl::CommandLine cl;
- //Make sure the configured temp folder exists
- QDir tempDir = QDir::temp();
- if (!tempDir.exists())
- tempDir.root().mkpath(tempDir.canonicalPath());
-
- //Should allow for better scaling of ui with higher resolution displays
- QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
-
- if (argc >= 4) {
- std::vector<std::wstring> arg;
- auto args = UntouchedCommandLineArguments(2, arg);
- if (arg[0] == L"launch")
- return SpawnWaitProcess(arg[1].c_str(), args);
+ if (auto r=cl.run(GetCommandLineW())) {
+ return *r;
}
- MOApplication application(argc, argv);
- QStringList arguments = application.arguments();
+ TimeThis tt("main() to doOneRun()");
env::prependToPath(QDir::toNativeSeparators(
QCoreApplication::applicationDirPath() + "/dlls"));
- SetThisThreadName("main");
+ initLogging();
+ auto application = MOApplication::create(argc, argv);
+ addDllsToPath();
- bool forcePrimary = false;
- if (arguments.contains("update")) {
- arguments.removeAll("update");
- forcePrimary = true;
+ SingleInstance instance(cl.multiple());
+ if (instance.ephemeral()) {
+ return forwardToPrimary(instance, cl);
}
- MOShortcut moshortcut{ arguments.size() > 1 ? arguments.at(1) : "" };
+ tt.stop();
- SingleInstance instance(forcePrimary);
- if (!instance.primaryInstance()) {
- if (moshortcut ||
- arguments.size() > 1 && OrganizerCore::isNxmLink(arguments.at(1)))
- {
- log::debug("not primary instance, sending shortcut/download message");
- instance.sendMessage(arguments.at(1));
- return 0;
- } else if (arguments.size() == 1) {
- QMessageBox::information(
- nullptr, QObject::tr("Mod Organizer"),
- QObject::tr("An instance of Mod Organizer is already running"));
- return 0;
- }
- } // we continue for the primary instance OR if MO was called with parameters
-
- do {
- 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({});
-
- QString dataPath;
-
- try {
- InstanceManager& instanceManager = InstanceManager::instance();
- if (moshortcut && moshortcut.hasInstance())
- instanceManager.overrideInstance(moshortcut.instance());
- dataPath = instanceManager.determineDataPath();
- } catch (const std::exception &e) {
- if (strcmp(e.what(),"Canceled"))
- QMessageBox::critical(nullptr, QObject::tr("Failed to set up instance"), e.what());
- return 1;
- }
- application.setProperty("dataPath", dataPath);
+ if (cl.instance())
+ InstanceManager::singleton().overrideInstance(*cl.instance());
- // initialize dump collection only after "dataPath" since the crashes are stored under it
- setUnhandledExceptionHandler();
-
- const auto logFile =
- qApp->property("dataPath").toString() + "/logs/mo_interface.log";
-
- if (!createAndMakeWritable(AppConfig::logPath())) {
- reportError("Failed to create log folder");
- InstanceManager::instance().clearCurrentInstance();
- return 1;
- }
-
- log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString()));
-
- log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
-
- QString splash = dataPath + "/splash.png";
- if (!QFile::exists(dataPath + "/splash.png")) {
- splash = ":/MO/gui/splash";
- }
+ if (cl.profile()) {
+ InstanceManager::singleton().overrideProfile(*cl.profile());
+ }
- tt.stop();
+ // makes plugin data path available to plugins, see
+ // IOrganizer::getPluginDataPath()
+ MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath());
- const int result = runApplication(application, instance, splash);
- if (result != RestartExitCode) {
- return result;
+ for (;;)
+ {
+ const auto r = doOneRun(cl, application, instance);
+ if (r == RestartExitCode) {
+ continue;
}
- argc = 1;
- moshortcut = MOShortcut("");
- } while (true);
+ return r;
+ }
}