summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/commandline.cpp108
-rw-r--r--src/commandline.h136
-rw-r--r--src/createinstancedialog.cpp367
-rw-r--r--src/createinstancedialog.h120
-rw-r--r--src/createinstancedialog.ui8
-rw-r--r--src/createinstancedialogpages.cpp469
-rw-r--r--src/createinstancedialogpages.h418
-rw-r--r--src/env.cpp27
-rw-r--r--src/env.h21
-rw-r--r--src/envdump.h30
-rw-r--r--src/instancemanager.cpp568
-rw-r--r--src/instancemanager.h312
-rw-r--r--src/instancemanagerdialog.cpp527
-rw-r--r--src/instancemanagerdialog.h93
-rw-r--r--src/instancemanagerdialog.ui8
-rw-r--r--src/loglist.cpp5
-rw-r--r--src/main.cpp747
-rw-r--r--src/mainwindow.cpp4
-rw-r--r--src/mainwindow.ui18
-rw-r--r--src/moapplication.cpp464
-rw-r--r--src/moapplication.h41
-rw-r--r--src/multiprocess.cpp (renamed from src/singleinstance.cpp)38
-rw-r--r--src/multiprocess.h70
-rw-r--r--src/organizercore.cpp48
-rw-r--r--src/organizercore.h13
-rw-r--r--src/plugincontainer.cpp73
-rw-r--r--src/sanitychecks.cpp10
-rw-r--r--src/sanitychecks.h27
-rw-r--r--src/settings.cpp20
-rw-r--r--src/settings.h37
-rw-r--r--src/settingsdialog.ui64
-rw-r--r--src/settingsdialogdiagnostics.cpp20
-rw-r--r--src/shared/appconfig.inc2
-rw-r--r--src/shared/error_report.cpp10
-rw-r--r--src/shared/error_report.h8
-rw-r--r--src/singleinstance.h90
-rw-r--r--src/usvfsconnector.cpp33
-rw-r--r--src/usvfsconnector.h3
39 files changed, 3268 insertions, 1792 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index eec33460..bb4b151f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -15,9 +15,9 @@ add_filter(NAME src/application GROUPS
main
moapplication
moshortcut
+ multiprocess
sanitychecks
selfupdater
- singleinstance
)
add_filter(NAME src/browser GROUPS
@@ -70,6 +70,7 @@ add_filter(NAME src/downloads GROUPS
add_filter(NAME src/env GROUPS
env
+ envdump
envfs
envmetrics
envmodule
diff --git a/src/commandline.cpp b/src/commandline.cpp
index 3f8a6b1a..fd2fcb51 100644
--- a/src/commandline.cpp
+++ b/src/commandline.cpp
@@ -1,10 +1,15 @@
#include "commandline.h"
#include "env.h"
+#include "organizercore.h"
#include "shared/util.h"
+#include <log.h>
+#include <report.h>
namespace cl
{
+using namespace MOBase;
+
std::string pad_right(std::string s, std::size_t n, char c=' ')
{
if (s.size() < n)
@@ -13,6 +18,8 @@ std::string pad_right(std::string s, std::size_t n, char c=' ')
return s;
}
+// formats the list of pairs in two columns
+//
std::string table(
const std::vector<std::pair<std::string, std::string>>& v,
std::size_t indent, std::size_t spacing)
@@ -60,6 +67,9 @@ std::optional<int> CommandLine::run(const std::wstring& line)
args.erase(args.begin());
}
+ // parsing the first part of the command line, including global options and
+ // command name, but not the rest, which will be collected below
+
auto parsed = po::wcommand_line_parser(args)
.options(m_allOptions)
.positional(m_positional)
@@ -69,20 +79,29 @@ std::optional<int> CommandLine::run(const std::wstring& line)
po::store(parsed, m_vm);
po::notify(m_vm);
+ // collect options past the command name
auto opts = po::collect_unrecognized(
parsed.options, po::include_positional);
if (m_vm.count("command")) {
+ // there's a word as the first argument; this may be a command name or
+ // an old style exe name/binary
+
const auto commandName = m_vm["command"].as<std::string>();
+ // look for the command by name first
for (auto&& c : m_commands) {
if (c->name() == commandName) {
+ // this is a command
+
// remove the command name itself
opts.erase(opts.begin());
try
{
+ // parse the the remainder of the command line according to the
+ // command's options
po::wcommand_line_parser parser(opts);
auto co = c->allOptions();
@@ -106,6 +125,7 @@ std::optional<int> CommandLine::run(const std::wstring& line)
return 0;
}
+ // run the command
return c->run(line, m_vm, opts);
}
catch(po::error& e)
@@ -122,6 +142,12 @@ std::optional<int> CommandLine::run(const std::wstring& line)
}
}
+
+ // the first word on the command line is not a valid command, try the other
+ // stuff; this is used in setupCore() below when called from
+ // MOApplication::doOneRun()
+
+ // look for help
if (m_vm.count("help")) {
env::Console console;
std::cout << usage() << "\n";
@@ -130,12 +156,25 @@ std::optional<int> CommandLine::run(const std::wstring& line)
if (!opts.empty()) {
const auto qs = QString::fromStdWString(opts[0]);
+
+ if (qs.startsWith("--")) {
+ // assume that for something like `ModOrganizer.exe --bleh`, it's just
+ // a bad option instead of an executable that starts with "--"
+ env::Console console;
+ std::cerr << "\nUnrecognized option " << qs.toStdString() << "\n";
+
+ return 1;
+ }
+
+ // try as an moshorcut://
m_shortcut = qs;
if (!m_shortcut.isValid()) {
+ // not a shortcut, try a link
if (isNxmLink(qs)) {
m_nxmLink = qs;
} else {
+ // assume an executable name/binary
m_executable = qs;
}
}
@@ -162,6 +201,52 @@ std::optional<int> CommandLine::run(const std::wstring& line)
}
}
+std::optional<int> CommandLine::setupCore(OrganizerCore& organizer) const
+{
+ if (m_shortcut.isValid()) {
+ if (m_shortcut.hasExecutable()) {
+ try {
+ organizer.processRunner()
+ .setFromShortcut(m_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 (m_nxmLink) {
+ log::debug("starting download from command line: {}", *m_nxmLink);
+ organizer.externalMessage(*m_nxmLink);
+ } else if (m_executable) {
+ const QString exeName = *m_executable;
+ log::debug("starting {} from command line", exeName);
+
+ try
+ {
+ // pass the remaining parameters to the binary
+ organizer.processRunner()
+ .setFromFileOrExecutable(exeName, m_untouched)
+ .setWaitForCompletion()
+ .run();
+
+ return 0;
+ }
+ catch (const std::exception &e)
+ {
+ reportError(
+ QObject::tr("failed to start application: %1").arg(e.what()));
+ return 1;
+ }
+ }
+
+ return {};
+}
+
void CommandLine::clear()
{
m_vm.clear();
@@ -182,6 +267,7 @@ void CommandLine::createOptions()
("command", po::value<std::string>(), "command")
("subargs", po::value<std::vector<std::string> >(), "args");
+ // one command name, followed by any arguments for that command
m_positional
.add("command", 1)
.add("subargs", -1);
@@ -210,6 +296,7 @@ std::string CommandLine::usage(const Command* c) const
<< "\n"
<< "Commands:\n";
+ // name and description for all commands
std::vector<std::pair<std::string, std::string>> v;
for (auto&& c : m_commands) {
v.push_back({c->name(), c->description()});
@@ -224,6 +311,7 @@ std::string CommandLine::usage(const Command* c) const
<< "Global options:\n"
<< m_visibleOptions << "\n";
+ // show the more text unless this is usage for a specific command
if (!c) {
oss << "\n" << more() << "\n";
}
@@ -247,6 +335,8 @@ std::optional<QString> CommandLine::profile() const
std::optional<QString> CommandLine::instance() const
{
+ // note that moshortcut:// overrides -i
+
if (m_shortcut.isValid() && m_shortcut.hasInstance()) {
return m_shortcut.instance();
} else if (m_vm.count("instance")) {
@@ -369,21 +459,6 @@ po::positional_options_description Command::getPositional() const
}
-std::string Command::usage() const
-{
- std::ostringstream oss;
-
- oss
- << "\n"
- << "Usage:\n"
- << " ModOrganizer.exe [options] [[command] [command-options]]\n"
- << "\n"
- << "Options:\n"
- << visibleOptions() << "\n";
-
- return oss.str();
-}
-
std::optional<int> Command::run(
const std::wstring& originalLine,
po::variables_map vm,
@@ -569,7 +644,7 @@ std::optional<int> ExeCommand::doRun()
const auto args = vm()["arguments"].as<std::string>();
const auto cwd = vm()["cwd"].as<std::string>();
-
+ std::cout << "not implemented\n";
return 0;
}
@@ -587,6 +662,7 @@ Command::Meta RunCommand::meta() const
std::optional<int> RunCommand::doRun()
{
+ std::cout << "not implemented\n";
return {};
}
diff --git a/src/commandline.h b/src/commandline.h
index 72018ba3..baa0bfb9 100644
--- a/src/commandline.h
+++ b/src/commandline.h
@@ -4,49 +4,103 @@
#include <vector>
#include <memory>
+class OrganizerCore;
+
namespace cl
{
namespace po = boost::program_options;
-
+// base class for all commands
+//
class Command
{
public:
virtual ~Command() = default;
+ // command name, used on the command line to invoke it; this is meta().name
+ //
std::string name() const;
+
+ // command short description, shown in general help; this is
+ // meta().description
+ //
std::string description() const;
+
+ // usage line, puts together the name and whatever getUsageLine() returns
+ //
std::string usageLine() const;
+
+ // returns all options for this command, including hidden ones; used to parse
+ // the command line
+ //
po::options_description allOptions() const;
+
+ // returns visible options, used to display usage
+ //
po::options_description visibleOptions() const;
+
+ // returns positional arguments
+ //
po::positional_options_description positional() const;
- std::string usage() const;
+ // whether the command allows for unregistered options; this is only used for
+ // the old `launch` command and shouldn't be used anywhere else
+ //
virtual bool allow_unregistered() const;
+ // runs this command, eventually calls doRun()
+ //
std::optional<int> run(
const std::wstring& originalLine,
po::variables_map vm,
std::vector<std::wstring> untouched);
protected:
+ // meta information about this command, returned by derived classes
+ //
struct Meta
{
std::string name, description;
};
+ // returns the usage line for this command, not including the name
+ //
virtual std::string getUsageLine() const;
+
+ // returns visible options specific to this command
+ //
virtual po::options_description getVisibleOptions() const;
+
+ // returns hidden options specific to this command
+ //
virtual po::options_description getInternalOptions() const;
+
+ // returns positional arguments specific to this command
+ //
virtual po::positional_options_description getPositional() const;
+
+ // meta
+ //
virtual Meta meta() const = 0;
+
+ // runs the command
+ //
virtual std::optional<int> doRun() = 0;
+
+ // returns the original command line
+ //
const std::wstring& originalCmd() const;
+
+ // variables
+ //
const po::variables_map& vm() const;
+
+ // returns unparsed options, only used by launch
+ //
const std::vector<std::wstring>& untouched() const;
private:
@@ -56,6 +110,8 @@ private:
};
+// generates a crash dump for another MO process
+//
class CrashDumpCommand : public Command
{
protected:
@@ -93,6 +149,8 @@ protected:
};
+// runs a configured executable
+//
class ExeCommand : public Command
{
protected:
@@ -105,6 +163,8 @@ protected:
};
+// runs an arbitrary executable
+//
class RunCommand : public Command
{
protected:
@@ -114,24 +174,96 @@ protected:
};
+// parses the command line and runs any given command
+//
+// the command line used to support a few commands but with no real conventions;
+// those are mostly preserved for backwards compatibility, but deprecated:
+//
+// - moshortcut:// for desktop/taskbar shortcuts, may contain an instance name
+// and a configured executable or arbitrary binary (still used in MO for
+// shortcuts)
+//
+// - nxm:// links
+//
+// - the name of a configured executable or path to binary, followed by
+// arbitrary parameters, forwarded to the program
+//
+// any command added CommandLine will unfortunately break any executable with
+// the same name: `ModOrganizer.exe run` used to launch a program named "run"
+// but will now execute the command "run"
+//
+// if moshortcut:// is detected and has an instance, it will override -i if both
+// are given
+//
+//
+// the command is used in two phases:
+//
+// 1) run() is called in main() shortly after startup; it parses the command
+// line and runs the given command, if any
+//
+// 2) if the command did not request to exit, the instance is loaded
+// in main() et al. and setupCore() is called; it handles moshortcut,
+// nxm links and starting executables/binaries
+//
+// either can return an exit code, which will make MO exit immediately
+//
class CommandLine
{
public:
CommandLine();
+ // parses the given command line and executes the appropriate command, if
+ // any
+ //
+ // returns an empty optional if execution should continue, or a return code
+ // if MO must quit
+ //
std::optional<int> run(const std::wstring& line);
+
+ // handles moshortcut, nxm links and starting processes
+ //
+ // returns an empty optional if execution should continue, or a return code
+ // if MO must quit
+ //
+ std::optional<int> setupCore(OrganizerCore& organizer) const;
+
+
+ // clears parsed options, used when MO is "restarted" so the options aren't
+ // processed again
+ //
void clear();
+ // global usage string plus usage for the given command, if any
+ //
std::string usage(const Command* c=nullptr) const;
+
+ // whether --multiple was given
+ //
bool multiple() const;
+
+ // profile override (-p)
+ //
std::optional<QString> profile() const;
+
+ // instance override (-i)
+ //
std::optional<QString> instance() const;
+ // returns the data parsed from an moshortcut:// option, if any
+ //
const MOShortcut& shortcut() const;
+
+ // returns the nxm:// link, if any
+ //
std::optional<QString> nxmLink() const;
+
+ // returns the executable/binary, if any
std::optional<QString> executable() const;
+ // returns the list of arguments, excluding moshortcut or executable name;
+ // deprecated, only use with executable()
+ //
const QStringList& untouched() const;
private:
diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp
index 2ff8adf3..47cb9d5c 100644
--- a/src/createinstancedialog.cpp
+++ b/src/createinstancedialog.cpp
@@ -10,6 +10,105 @@
using namespace MOBase;
+
+class Failed {};
+
+// create() will create all the directories in `target`; if any path component
+// fails to create, it will throw Failed
+//
+// unless commit() is called, all the created directories will be deleted in
+// the destructor
+//
+class DirectoryCreator
+{
+public:
+ DirectoryCreator(const DirectoryCreator&) = delete;
+ DirectoryCreator& operator=(const DirectoryCreator&) = delete;
+
+ static std::unique_ptr<DirectoryCreator> create(
+ const QDir& target, std::function<void (QString)> log)
+ {
+ return std::unique_ptr<DirectoryCreator>(new DirectoryCreator(target, log));
+ }
+
+ ~DirectoryCreator()
+ {
+ rollback();
+ }
+
+ void commit()
+ {
+ m_created.clear();
+ }
+
+ void rollback() noexcept
+ {
+ try
+ {
+ // delete each directory starting from the end
+ for (auto itor=m_created.rbegin(); itor!=m_created.rend(); ++itor) {
+ const auto r = shell::DeleteDirectoryRecursive(*itor);
+ if (!r) {
+ m_logger(r.toString());
+ }
+ }
+
+ m_created.clear();
+ }
+ catch(...)
+ {
+ // eat it
+ }
+ }
+
+private:
+ std::function<void (QString)> m_logger;
+
+ DirectoryCreator(const QDir& target, std::function<void (QString)> log)
+ : m_logger(log)
+ {
+ try
+ {
+ // split on separators
+ const QString s = QDir::toNativeSeparators(target.absolutePath());
+ const QStringList cs = s.split("\\");
+
+ if (cs.empty()) {
+ return;
+ }
+
+ // root directory
+ QDir d(cs[0]);
+
+ // for each directory after the root
+ for (int i=1; i<cs.size(); ++i) {
+ d = d.filePath(cs[i]);
+
+ if (!d.exists()) {
+ m_logger(QObject::tr("Creating %1").arg(d.path()));
+ const auto r = shell::CreateDirectories(d);
+
+ if (!r) {
+ m_logger(r.toString());
+ throw Failed();
+ }
+
+ m_created.push_back(d);
+ }
+ }
+ }
+ catch(...)
+ {
+ rollback();
+ throw;
+ }
+ }
+
+private:
+ std::vector<QDir> m_created;
+};
+
+
CreateInstanceDialog::CreateInstanceDialog(
const PluginContainer& pc, Settings* s, QWidget *parent) :
QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc), m_settings(s),
@@ -42,8 +141,16 @@ CreateInstanceDialog::CreateInstanceDialog(
next();
}
+ ui->next->setFocus();
+
updateNavigation();
+ addShortcutAction(QKeySequence::Find, Actions::Find);
+
+ addShortcut(Qt::ALT+Qt::Key_Left, [&]{ back(); });
+ addShortcut(Qt::ALT+Qt::Key_Right, [&]{ next(false); });
+ addShortcut(Qt::CTRL+Qt::Key_Return, [&]{ next(); });
+
connect(ui->next, &QPushButton::clicked, [&]{ next(); });
connect(ui->back, &QPushButton::clicked, [&]{ back(); });
connect(ui->cancel, &QPushButton::clicked, [&]{ reject(); });
@@ -77,17 +184,23 @@ bool CreateInstanceDialog::isOnLastPage() const
return true;
}
-void CreateInstanceDialog::next()
+void CreateInstanceDialog::next(bool allowFinish)
{
+ if (!canNext()) {
+ return;
+ }
+
const auto i = ui->pages->currentIndex();
const auto last = isOnLastPage();
if (last) {
- if (m_singlePage) {
- // just close the dialog
- accept();
- } else {
- finish();
+ if (allowFinish) {
+ if (m_singlePage) {
+ // just close the dialog
+ accept();
+ } else {
+ finish();
+ }
}
} else {
changePage(+1);
@@ -96,9 +209,40 @@ void CreateInstanceDialog::next()
void CreateInstanceDialog::back()
{
+ if (!canBack()) {
+ return;
+ }
+
changePage(-1);
}
+void CreateInstanceDialog::addShortcut(
+ QKeySequence seq, std::function<void ()> f)
+{
+ auto* sc = new QShortcut(seq, this);
+
+ sc->setAutoRepeat(false);
+ sc->setContext(Qt::WidgetWithChildrenShortcut);
+
+ QObject::connect(sc, &QShortcut::activated, f);
+}
+
+void CreateInstanceDialog::addShortcutAction(QKeySequence seq, Actions a)
+{
+ addShortcut(seq, [this, a]{ doAction(a); });
+}
+
+void CreateInstanceDialog::doAction(Actions a)
+{
+ std::size_t i = static_cast<std::size_t>(ui->pages->currentIndex());
+
+ if (i >= m_pages.size()) {
+ return;
+ }
+
+ m_pages[i]->action(a);
+}
+
void CreateInstanceDialog::setSinglePageImpl(const QString& instanceName)
{
m_singlePage = true;
@@ -121,6 +265,7 @@ void CreateInstanceDialog::changePage(int d)
// first/last page
if (d > 0) {
+ // forwards
for (;;) {
++i;
@@ -133,6 +278,7 @@ void CreateInstanceDialog::changePage(int d)
}
}
} else {
+ // backwards
for (;;) {
if (i == 0) {
break;
@@ -151,94 +297,6 @@ void CreateInstanceDialog::changePage(int d)
}
}
-
-class Failed {};
-
-class DirectoryCreator
-{
-public:
- DirectoryCreator(const DirectoryCreator&) = delete;
- DirectoryCreator& operator=(const DirectoryCreator&) = delete;
-
- static std::unique_ptr<DirectoryCreator> create(
- const QDir& target, std::function<void (QString)> log)
- {
- return std::unique_ptr<DirectoryCreator>(new DirectoryCreator(target, log));
- }
-
- ~DirectoryCreator()
- {
- rollback();
- }
-
- void commit()
- {
- m_created.clear();
- }
-
- void rollback() noexcept
- {
- try
- {
- for (auto itor=m_created.rbegin(); itor!=m_created.rend(); ++itor) {
- const auto r = shell::DeleteDirectoryRecursive(*itor);
- if (!r) {
- m_logger(r.toString());
- }
- }
-
- m_created.clear();
- }
- catch(...)
- {
- // eat it
- }
- }
-
-private:
- std::function<void (QString)> m_logger;
-
- DirectoryCreator(const QDir& target, std::function<void (QString)> log)
- : m_logger(log)
- {
- try
- {
- const QString s = QDir::toNativeSeparators(target.absolutePath());
- const QStringList cs = s.split("\\");
-
- if (cs.empty()) {
- return;
- }
-
- QDir d(cs[0]);
-
- for (int i=1; i<cs.size(); ++i) {
- d = d.filePath(cs[i]);
-
- if (!d.exists()) {
- m_logger(QObject::tr("Creating %1").arg(d.path()));
- const auto r = shell::CreateDirectories(d);
-
- if (!r) {
- m_logger(r.toString());
- throw Failed();
- }
-
- m_created.push_back(d);
- }
- }
- }
- catch(...)
- {
- rollback();
- throw;
- }
- }
-
-private:
- std::vector<QDir> m_created;
-};
-
void CreateInstanceDialog::finish()
{
ui->creationLog->clear();
@@ -260,6 +318,8 @@ void CreateInstanceDialog::finish()
{
std::vector<std::unique_ptr<DirectoryCreator>> dirs;
+ // creating all these directories; if any of them fail, this throws and
+ // any newly created directory will be deleted in DirectoryCreator's dtor
dirs.push_back(createDir(ci.dataPath));
dirs.push_back(createDir(ci.paths.base));
dirs.push_back(createDir(PathSettings::resolve(ci.paths.downloads, ci.paths.base)));
@@ -268,6 +328,7 @@ void CreateInstanceDialog::finish()
dirs.push_back(createDir(PathSettings::resolve(ci.paths.overwrite, ci.paths.base)));
+ // creating ini
Settings s(ci.iniPath);
s.game().setName(ci.game->gameName());
s.game().setDirectory(ci.gameLocation);
@@ -301,7 +362,9 @@ void CreateInstanceDialog::finish()
logCreation(tr("Writing %1...").arg(ci.iniPath));
+ // writing ini
const auto r = s.sync();
+
if (r != QSettings::NoError) {
switch (r)
{
@@ -321,16 +384,15 @@ void CreateInstanceDialog::finish()
throw Failed();
}
+
+ // committing all the directories so they don't get deleted
for (auto& d : dirs) {
d->commit();
}
logCreation(tr("Done."));
- if (ui->hideIntro->isChecked()) {
- GlobalSettings::setHideCreateInstanceIntro(true);
- }
-
+ // launch the new instance
if (ui->launch->isChecked()) {
InstanceManager::singleton().setCurrentInstance(ci.instanceName);
@@ -338,15 +400,16 @@ void CreateInstanceDialog::finish()
// don't restart without settings, it happens on startup when there are
// no instances
ExitModOrganizer(Exit::Restart);
+ m_switching = true;
}
-
- m_switching = true;
}
+ // close the dialog
accept();
}
catch(Failed&)
{
+ // if Failed was thrown, all the directories have been deleted
}
}
@@ -413,89 +476,67 @@ bool CreateInstanceDialog::canBack() const
return false;
}
-CreateInstanceDialog::Types CreateInstanceDialog::instanceType() const
-{
- return getSelected(&cid::Page::selectedInstanceType);
-}
-
-MOBase::IPluginGame* CreateInstanceDialog::game() const
-{
- return getSelected(&cid::Page::selectedGame);
-}
-
-QString CreateInstanceDialog::gameLocation() const
+bool CreateInstanceDialog::switching() const
{
- return getSelected(&cid::Page::selectedGameLocation);
+ return m_switching;
}
-QString CreateInstanceDialog::gameVariant() const
+CreateInstanceDialog::CreationInfo
+CreateInstanceDialog::rawCreationInfo() const
{
- return getSelected(&cid::Page::selectedGameVariant);
-}
+ const auto iniFilename = QString::fromStdWString(AppConfig::iniFileName());
-QString CreateInstanceDialog::instanceName() const
-{
- return getSelected(&cid::Page::selectedInstanceName);
-}
+ CreationInfo ci;
-QString CreateInstanceDialog::dataPath() const
-{
- QString s;
+ ci.type = getSelected(&cid::Page::selectedInstanceType);
+ ci.game = getSelected(&cid::Page::selectedGame);
+ ci.gameLocation = getSelected(&cid::Page::selectedGameLocation);
+ ci.gameVariant = getSelected(&cid::Page::selectedGameVariant, ci.game);
+ ci.instanceName = getSelected(&cid::Page::selectedInstanceName);
+ ci.paths = getSelected(&cid::Page::selectedPaths);
- if (instanceType() == Portable) {
- s = QDir(InstanceManager::portablePath()).absolutePath();
+ if (ci.type == Portable) {
+ ci.dataPath = QDir(InstanceManager::singleton().portablePath()).absolutePath();
} else {
- s = InstanceManager::singleton().instancePath(instanceName());
+ ci.dataPath = InstanceManager::singleton().instancePath(ci.instanceName);
}
- return QDir::toNativeSeparators(s);
-}
-
-CreateInstanceDialog::Paths CreateInstanceDialog::paths() const
-{
- return getSelected(&cid::Page::selectedPaths);
-}
+ ci.dataPath = QDir::toNativeSeparators(ci.dataPath);
+ ci.iniPath = ci.dataPath + "/" + iniFilename;
-bool CreateInstanceDialog::switching() const
-{
- return m_switching;
+ return ci;
}
-void fixVarDir(QString& path, const std::wstring& defaultDir)
+CreateInstanceDialog::CreationInfo
+CreateInstanceDialog::creationInfo() const
{
- if (path.isEmpty()) {
- path = cid::makeDefaultPath(defaultDir);
- } else if (!path.contains(PathSettings::BaseDirVariable)) {
- path = QDir(path).absolutePath();
- }
+ auto fixVarDir = [](QString& path, const std::wstring& defaultDir)
+ {
+ // if the path is empty, it wasn't filled by the user, probably because
+ // the "Advanced" checkbox wasn't checked, so use the base dir variable
+ // with the default dir
- path = QDir::toNativeSeparators(path);
-}
+ if (path.isEmpty()) {
+ path = cid::makeDefaultPath(defaultDir);
+ } else if (!path.contains(PathSettings::BaseDirVariable)) {
+ path = QDir(path).absolutePath();
+ }
-void fixDirPath(QString& path)
-{
- path = QDir::toNativeSeparators(QDir(path).absolutePath());
-}
+ path = QDir::toNativeSeparators(path);
+ };
-void fixFilePath(QString& path)
-{
- path = QDir::toNativeSeparators(QFileInfo(path).absolutePath());
-}
+ auto fixDirPath = [](QString& path)
+ {
+ path = QDir::toNativeSeparators(QDir(path).absolutePath());
+ };
-CreateInstanceDialog::CreationInfo CreateInstanceDialog::creationInfo() const
-{
- const auto iniFilename = QString::fromStdWString(AppConfig::iniFileName());
+ auto fixFilePath = [](QString& path)
+ {
+ path = QDir::toNativeSeparators(QFileInfo(path).absolutePath());
+ };
- CreationInfo ci;
- ci.type = getSelected(&cid::Page::selectedInstanceType);
- ci.game = getSelected(&cid::Page::selectedGame);
- ci.gameLocation = getSelected(&cid::Page::selectedGameLocation);
- ci.gameVariant = getSelected(&cid::Page::selectedGameVariant);
- ci.instanceName = getSelected(&cid::Page::selectedInstanceName);
- ci.paths = getSelected(&cid::Page::selectedPaths);
- ci.dataPath = dataPath();
- ci.iniPath = ci.dataPath + "/" + iniFilename;
+ auto ci = rawCreationInfo();
fixDirPath(ci.paths.base);
fixFilePath(ci.paths.ini);
diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h
index 25e383eb..8e8fe517 100644
--- a/src/createinstancedialog.h
+++ b/src/createinstancedialog.h
@@ -10,11 +10,30 @@ namespace cid { class Page; }
class PluginContainer;
class Settings;
+// this is a wizard for creating a new instance, it is made out of Page objects,
+// see createinstancedialogpages.h
+//
+// each page can give back one or more pieces of information that is collected
+// in creationInfo() and used by finish() to do the actual creation
+//
+// pages can be disabled if they return true in skip(), which happens globally
+// for some (IntroPage has a setting in the registry), depending on context
+// (NexusPage is skipped if the API key already exists) or explicitly (when
+// only some info about the instance is missing on startup, such as a game
+// variant)
+//
class CreateInstanceDialog : public QDialog
{
Q_OBJECT
public:
+ enum class Actions
+ {
+ Find = 1
+ };
+
+ // instance type
+ //
enum Types
{
NoType = 0,
@@ -22,6 +41,10 @@ public:
Portable
};
+ // all the paths required by the instance, some may be empty, such as
+ // basically all of them except for `base` when the user doesn't use the
+ // "Advanced" part of the paths page
+ //
struct Paths
{
QString base;
@@ -34,6 +57,8 @@ public:
auto operator<=>(const Paths&) const = default;
};
+ // all the info filled in the various pages
+ //
struct CreationInfo
{
Types type;
@@ -53,10 +78,11 @@ public:
~CreateInstanceDialog();
Ui::CreateInstanceDialog* getUI();
-
const PluginContainer& pluginContainer();
Settings* settings();
+ // disables all the pages except for the given one, used on startup when some
+ // specific info is missing
template <class Page>
void setSinglePage(const QString& instanceName)
{
@@ -71,6 +97,8 @@ public:
setSinglePageImpl(instanceName);
}
+ // returns the page having the give path, or null
+ //
template <class Page>
Page* getPage()
{
@@ -83,24 +111,60 @@ public:
return nullptr;
}
- void next();
+
+ // moves to the next page; if `allowFinish` is true, calls finish() if
+ // currently on the last page
+ //
+ void next(bool allowFinish=true);
+
+ // moves to the previous page, if any
+ //
void back();
+
+ // whether the current page reports that it is ready; if this is the last
+ // page, next() would call finish()
+ //
+ bool canNext() const;
+
+ // whether the current page is not the first one and there is an enabled page
+ // prior
+ //
+ bool canBack() const;
+
+ // selects the given page by index; this doesn't check if the page should be
+ // skipped
+ //
void selectPage(std::size_t i);
+
+ // moves by `d` pages, can be negative to move back
+ //
void changePage(int d);
+
+ // creates the instance and closes the dialog
+ //
void finish();
+
+ // updates the navigation buttons based on the current page
+ //
void updateNavigation();
+
+ // whether this is the last enabled page
+ //
bool isOnLastPage() const;
- Types instanceType() const;
- MOBase::IPluginGame* game() const;
- QString gameLocation() const;
- QString gameVariant() const;
- QString instanceName() const;
- QString dataPath() const;
- Paths paths() const;
+ // returns whether the user has requested to switch to the new instance
+ //
bool switching() const;
+ // gathers the info from all the pages as it appears, paths are not fixed;
+ // see creationInfo()
+ //
+ CreationInfo rawCreationInfo() const;
+
+ // gathers the info from all the pages: paths are converted to absolute and
+ // the base dir variable is expanded everywhere; see rawCreationInfo()
+ //
CreationInfo creationInfo() const;
private:
@@ -113,13 +177,39 @@ private:
bool m_singlePage;
+ // creates a shortcut for the given sequence
+ //
+ void addShortcut(QKeySequence seq, std::function<void ()> f);
+
+ // creates a shortcut for the given sequence and executes the action when
+ // activated
+ //
+ void addShortcutAction(QKeySequence seq, Actions a);
+
+ // calls action() with the given action on the selected page, if any
+ //
+ void doAction(Actions a);
+
+ // called from setSinglePage(), does whatever doesn't need the T
+ //
void setSinglePageImpl(const QString& instanceName);
- template <class T>
- T getSelected(T (cid::Page::*mf)() const) const
+ // adds a line to the creation log
+ //
+ void logCreation(const QString& s);
+ void logCreation(const std::wstring& s);
+
+ // calls the given member function on all pages until one returns an object
+ // that's not empty; used by gatherInfo()
+ //
+ template <class MF, class... Args>
+ auto getSelected(MF mf, Args&&... args) const
{
+ // return type
+ using T = decltype((std::declval<cid::Page>().*mf)(std::forward<Args>(args)...));
+
for (auto&& p : m_pages) {
- const auto t = (p.get()->*mf)();
+ const auto t = (p.get()->*mf)(std::forward<Args>(args)...);
if (t != T()) {
return t;
}
@@ -127,12 +217,6 @@ private:
return T();
}
-
- void logCreation(const QString& s);
- void logCreation(const std::wstring& s);
-
- bool canNext() const;
- bool canBack() const;
};
#endif // MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED
diff --git a/src/createinstancedialog.ui b/src/createinstancedialog.ui
index b10d74a7..86fa900b 100644
--- a/src/createinstancedialog.ui
+++ b/src/createinstancedialog.ui
@@ -86,7 +86,7 @@
<item>
<widget class="LinkLabel" name="label_19">
<property name="text">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer/wiki/Instance&quot;&gt;More information&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer/wiki/Instances&quot;&gt;More information&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
@@ -1102,6 +1102,9 @@
<property name="lineWrapMode">
<enum>QTextEdit::NoWrap</enum>
</property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item>
@@ -1112,6 +1115,9 @@
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
+ <property name="placeholderText">
+ <string>Instance creation log</string>
+ </property>
</widget>
</item>
<item>
diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp
index 26f2f61d..882776df 100644
--- a/src/createinstancedialogpages.cpp
+++ b/src/createinstancedialogpages.cpp
@@ -15,12 +15,29 @@ namespace cid
using namespace MOBase;
using MOBase::TaskDialog;
+// returns %base_dir%/dir
+//
QString makeDefaultPath(const std::wstring& dir)
{
return QDir::toNativeSeparators(PathSettings::makeDefaultPath(
QString::fromStdWString(dir)));
}
+QString toLocalizedString(CreateInstanceDialog::Types t)
+{
+ switch (t)
+ {
+ case CreateInstanceDialog::Global:
+ return QObject::tr("Global");
+
+ case CreateInstanceDialog::Portable:
+ return QObject::tr("Portable");
+
+ default:
+ return QObject::tr("Instance type: %1").arg(QObject::tr("?"));
+ }
+}
+
PlaceholderLabel::PlaceholderLabel(QLabel* label)
@@ -42,8 +59,9 @@ void PlaceholderLabel::setVisible(bool b)
-Page::Page(CreateInstanceDialog& dlg)
- : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()), m_skip(false)
+Page::Page(CreateInstanceDialog& dlg) :
+ ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()),
+ m_skip(false), m_firstActivation(true)
{
}
@@ -54,6 +72,7 @@ bool Page::ready() const
bool Page::skip() const
{
+ // setSkip() overrides this if it's true
return m_skip || doSkip();
}
@@ -62,11 +81,17 @@ bool Page::doSkip() const
return false;
}
-void Page::activated()
+void Page::doActivated(bool)
{
// no-op
}
+void Page::activated()
+{
+ doActivated(m_firstActivation);
+ m_firstActivation = false;
+}
+
void Page::setSkip(bool b)
{
m_skip = b;
@@ -82,6 +107,12 @@ void Page::next()
m_dlg.next();
}
+bool Page::action(CreateInstanceDialog::Actions a)
+{
+ // no-op
+ return false;
+}
+
CreateInstanceDialog::Types Page::selectedInstanceType() const
{
@@ -101,7 +132,7 @@ QString Page::selectedGameLocation() const
return {};
}
-QString Page::selectedGameVariant() const
+QString Page::selectedGameVariant(MOBase::IPluginGame*) const
{
// no-op
return {};
@@ -121,27 +152,32 @@ CreateInstanceDialog::Paths Page::selectedPaths() const
IntroPage::IntroPage(CreateInstanceDialog& dlg)
- : Page(dlg)
+ : Page(dlg), m_skip(GlobalSettings::hideCreateInstanceIntro())
{
+ QObject::connect(ui->hideIntro, &QCheckBox::toggled, [&] {
+ GlobalSettings::setHideCreateInstanceIntro(ui->hideIntro->isChecked());
+ });
}
bool IntroPage::doSkip() const
{
- return GlobalSettings::hideCreateInstanceIntro();
+ return m_skip;
}
TypePage::TypePage(CreateInstanceDialog& dlg)
: Page(dlg), m_type(CreateInstanceDialog::NoType)
{
+ // replace placeholders with actual paths
ui->createGlobal->setDescription(
ui->createGlobal->description()
- .arg(InstanceManager::singleton().instancesPath()));
+ .arg(InstanceManager::singleton().globalInstancesRootPath()));
ui->createPortable->setDescription(
ui->createPortable->description()
- .arg(InstanceManager::portablePath()));
+ .arg(InstanceManager::singleton().portablePath()));
+ // disable portable button if it already exists
if (InstanceManager::singleton().portableInstanceExists()) {
ui->createPortable->setEnabled(false);
ui->portableExistsLabel->setVisible(true);
@@ -187,6 +223,13 @@ void TypePage::portable()
next();
}
+void TypePage::doActivated(bool firstTime)
+{
+ if (firstTime) {
+ ui->createGlobal->setFocus();
+ }
+}
+
GamePage::Game::Game(IPluginGame* g)
: game(g), installed(g->isInstalled())
@@ -204,6 +247,7 @@ GamePage::GamePage(CreateInstanceDialog& dlg)
fillList();
m_filter.setEdit(ui->gamesFilter);
+ m_filter.setUpdateDelay(0);
QObject::connect(&m_filter, &FilterWidget::changed, [&]{ fillList(); });
QObject::connect(ui->showAllGames, &QCheckBox::clicked, [&]{ fillList(); });
@@ -214,6 +258,18 @@ bool GamePage::ready() const
return (m_selection != nullptr);
}
+bool GamePage::action(CreateInstanceDialog::Actions a)
+{
+ using Actions = CreateInstanceDialog::Actions;
+
+ if (a == Actions::Find) {
+ ui->gamesFilter->setFocus();
+ return true;
+ }
+
+ return false;
+}
+
IPluginGame* GamePage::selectedGame() const
{
if (!m_selection) {
@@ -239,26 +295,50 @@ void GamePage::select(IPluginGame* game, const QString& dir)
if (checked) {
if (!checked->installed) {
if (dir.isEmpty()) {
+ // the selected game has no installation directory and none was given,
+ // ask the user
+
const auto path = QFileDialog::getExistingDirectory(
- &m_dlg, QObject::tr("Find game installation"));
+ &m_dlg, QObject::tr("Find game installation for %1")
+ .arg(game->gameName()));
if (path.isEmpty()) {
+ // cancelled
checked = nullptr;
} else {
+ // check whether a plugin supports the given directory; this can
+ // return the same plugin, a different one, or null
checked = checkInstallation(path, checked);
+
+ if (checked) {
+ // plugin was found, remember this path
+ checked->dir = path;
+ checked->installed = true;
+ }
}
} else {
+ // the selected game didn't detect anything, but a directory was given,
+ // so use that
checked->dir = dir;
checked->installed = true;
}
}
}
+
+ // select this plugin, if any
m_selection = checked;
+
+ // update the button associated with it in case the paths have changed
+ updateButton(checked);
+
+ // toggle it on
selectButton(checked);
+
updateNavigation();
if (checked) {
+ // automatically move to the next page when a game is selected
next();
}
}
@@ -274,22 +354,33 @@ void GamePage::selectCustom()
return;
}
+ // try to find a plugin that likes this directory
for (auto& g : m_games) {
if (g->game->looksValid(path)) {
+ // found one
g->dir = path;
g->installed = true;
+
+ // select it
select(g->game);
+
+ // update the button because the path has changed
updateButton(g.get());
+
return;
}
}
+ // warning to the user
warnUnrecognized(path);
+
+ // reselect the previous button
selectButton(m_selection);
}
void GamePage::warnUnrecognized(const QString& path)
{
+ // put the list of supported games in the details textbox
QString supportedGames;
for (auto* game : sortedGamePlugins()) {
supportedGames += game->gameName() + "\n";
@@ -314,10 +405,12 @@ std::vector<IPluginGame*> GamePage::sortedGamePlugins() const
{
std::vector<IPluginGame*> v;
+ // all game plugins
for (auto* game : m_pc.plugins<IPluginGame>()) {
v.push_back(game);
}
+ // natsort
std::sort(v.begin(), v.end(), [](auto* a, auto* b) {
return (naturalCompare(a->gameName(), b->gameName()) < 0);
});
@@ -325,6 +418,15 @@ std::vector<IPluginGame*> GamePage::sortedGamePlugins() const
return v;
}
+void GamePage::createGames()
+{
+ m_games.clear();
+
+ for (auto* game : sortedGamePlugins()) {
+ m_games.push_back(std::make_unique<Game>(game));
+ }
+}
+
GamePage::Game* GamePage::findGame(IPluginGame* game)
{
for (auto& g : m_games) {
@@ -336,13 +438,24 @@ GamePage::Game* GamePage::findGame(IPluginGame* game)
return nullptr;
}
-void GamePage::createGames()
+void GamePage::createGameButton(Game* g)
{
- m_games.clear();
+ g->button = new QCommandLinkButton;
+ g->button->setCheckable(true);
- for (auto* game : sortedGamePlugins()) {
- m_games.push_back(std::make_unique<Game>(game));
- }
+ updateButton(g);
+
+ QObject::connect(g->button, &QAbstractButton::clicked, [g, this] {
+ select(g->game);
+ });
+}
+
+void GamePage::addButton(QAbstractButton* b)
+{
+ auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
+
+ // insert before the stretch
+ ly->insertWidget(ly->count() - 1, b);
}
void GamePage::updateButton(Game* g)
@@ -399,6 +512,31 @@ void GamePage::selectButton(Game* g)
}
}
+void GamePage::clearButtons()
+{
+ auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
+
+ ui->games->setUpdatesEnabled(false);
+
+ // delete all children
+ qDeleteAll(ui->games->findChildren<QWidget*>("", Qt::FindDirectChildrenOnly));
+
+ // stretch widgets added with addStretch() are not in the parent widget,
+ // they have to be deleted from the layout itself
+ while (auto* child=ly->takeAt(0))
+ delete child;
+
+ // add a stretch, buttons will be added before
+ ly->addStretch();
+
+ ui->games->setUpdatesEnabled(true);
+
+ for (auto& g : m_games) {
+ // all buttons have been deleted
+ g->button = nullptr;
+ }
+}
+
QCommandLinkButton* GamePage::createCustomButton()
{
auto* b = new QCommandLinkButton;
@@ -414,69 +552,39 @@ QCommandLinkButton* GamePage::createCustomButton()
return b;
}
-void GamePage::createGameButton(Game* g)
-{
- g->button = new QCommandLinkButton;
- g->button->setCheckable(true);
-
- updateButton(g);
-
- QObject::connect(g->button, &QAbstractButton::clicked, [g, this] {
- select(g->game);
- });
-}
-
void GamePage::fillList()
{
const bool showAll = ui->showAllGames->isChecked();
clearButtons();
- for (auto& g : m_games) {
- g->button = nullptr;
+ Game* firstButton = nullptr;
+ for (auto& g : m_games) {
if (!showAll && !g->installed) {
// not installed
continue;
}
if (!m_filter.matches(g->game->gameName())) {
+ // filtered out
continue;
}
createGameButton(g.get());
addButton(g->button);
+
+ if (!firstButton) {
+ firstButton = g.get();
+ }
}
+ // browse button
addButton(createCustomButton());
-}
-
-void GamePage::clearButtons()
-{
- auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
-
- ui->games->setUpdatesEnabled(false);
-
- // delete all children
- qDeleteAll(ui->games->findChildren<QWidget*>("", Qt::FindDirectChildrenOnly));
- // stretch widgets added with addStretch() are not in the parent widget,
- // they have to be deleted from the layout itself
- while (auto* child=ly->takeAt(0))
- delete child;
-
- // add a stretch, buttons will be added before
- ly->addStretch();
-
- ui->games->setUpdatesEnabled(true);
-}
-
-void GamePage::addButton(QAbstractButton* b)
-{
- auto* ly = static_cast<QVBoxLayout*>(ui->games->layout());
-
- // insert before the stretch
- ly->insertWidget(ly->count() - 1, b);
+ if (firstButton) {
+ firstButton->button->setDefault(true);
+ }
}
GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
@@ -487,13 +595,22 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
}
// the selected game can't use that folder, find another one
- auto* otherGame = findAnotherGame(path);
+ IPluginGame* otherGame = nullptr;
+
+ for (auto* gg : m_pc.plugins<IPluginGame>()) {
+ if (gg->looksValid(path)) {
+ otherGame = gg;
+ break;
+ }
+ }
+
if (otherGame == g->game) {
// shouldn't happen, but okay
return g;
}
if (otherGame) {
+ // an alternative was found, ask the user about it
auto* confirmedGame = confirmOtherGame(path, g->game, otherGame);
if (!confirmedGame) {
@@ -523,17 +640,6 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
return g;
}
-IPluginGame* GamePage::findAnotherGame(const QString& path)
-{
- for (auto* otherGame : m_pc.plugins<IPluginGame>()) {
- if (otherGame->looksValid(path)) {
- return otherGame;
- }
- }
-
- return nullptr;
-}
-
bool GamePage::confirmUnknown(const QString& path, IPluginGame* game)
{
const auto r = TaskDialog(&m_dlg)
@@ -606,26 +712,29 @@ VariantsPage::VariantsPage(CreateInstanceDialog& dlg)
bool VariantsPage::ready() const
{
+ // note that this isn't called when doSkip() is true, which happens when
+ // the game has no variants
+
return !m_selection.isEmpty();
}
bool VariantsPage::doSkip() const
{
- auto* g = m_dlg.game();
+ auto* g = m_dlg.rawCreationInfo().game;
if (!g) {
// shouldn't happen
return true;
}
- const auto variants = g->gameVariants();
- return (variants.size() < 2);
+ return (g->gameVariants().size() < 2);
}
-void VariantsPage::activated()
+void VariantsPage::doActivated(bool)
{
- auto* g = m_dlg.game();
+ auto* g = m_dlg.rawCreationInfo().game;
if (m_previousGame != g) {
+ // recreate the list, the game has changed
m_previousGame = g;
m_selection = "";
fillList();
@@ -634,9 +743,11 @@ void VariantsPage::activated()
void VariantsPage::select(const QString& variant)
{
+ m_selection = variant;
+
+ // find the button, set it checked
for (auto* b : m_buttons) {
if (b->text() == variant) {
- m_selection = variant;
b->setChecked(true);
} else {
b->setChecked(false);
@@ -646,20 +757,18 @@ void VariantsPage::select(const QString& variant)
updateNavigation();
if (!m_selection.isEmpty()) {
+ // automatically move to the next page when a variant is selected
next();
}
}
-QString VariantsPage::selectedGameVariant() const
+QString VariantsPage::selectedGameVariant(MOBase::IPluginGame* game) const
{
- auto* g = m_dlg.game();
- if (!g) {
- // shouldn't happen
+ if (!game) {
return {};
}
- const auto variants = g->gameVariants();
- if (variants.size() < 2) {
+ if (game->gameVariants().size() < 2) {
return {};
} else {
return m_selection;
@@ -671,14 +780,14 @@ void VariantsPage::fillList()
ui->editions->clear();
m_buttons.clear();
- auto* g = m_dlg.game();
+ auto* g = m_dlg.rawCreationInfo().game;
if (!g) {
// shouldn't happen
return;
}
- const auto variants = g->gameVariants();
- for (auto& v : variants) {
+ // for each variant, create a checkable button and add it
+ for (auto& v : g->gameVariants()) {
auto* b = new QCommandLinkButton(v);
b->setCheckable(true);
@@ -689,6 +798,10 @@ void VariantsPage::fillList()
ui->editions->addButton(b, QDialogButtonBox::AcceptRole);
m_buttons.push_back(b);
}
+
+ if (!m_buttons.empty()) {
+ m_buttons[0]->setDefault(true);
+ }
}
@@ -699,21 +812,26 @@ NamePage::NamePage(CreateInstanceDialog& dlg) :
{
QObject::connect(
ui->instanceName, &QLineEdit::textEdited, [&]{ onChanged(); });
+
+ QObject::connect(
+ ui->instanceName, &QLineEdit::returnPressed, [&]{ next(); });
}
bool NamePage::ready() const
{
+ // checked when textboxes change or when the page is activated
return m_okay;
}
bool NamePage::doSkip() const
{
- return (m_dlg.instanceType() == CreateInstanceDialog::Portable);
+ // portable instances have no name
+ return (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable);
}
-void NamePage::activated()
+void NamePage::doActivated(bool)
{
- auto* g = m_dlg.game();
+ auto* g = m_dlg.rawCreationInfo().game;
if (!g) {
// shouldn't happen, next should be disabled
return;
@@ -721,13 +839,15 @@ void NamePage::activated()
m_label.setText(g->gameName());
+ // generate a name if the user hasn't changed the text in case the game
+ // changed, or if it's empty
if (!m_modified || ui->instanceName->text().isEmpty()) {
const auto n = InstanceManager::singleton().makeUniqueName(g->gameName());
ui->instanceName->setText(n);
m_modified = false;
}
- updateWarnings();
+ verify();
}
QString NamePage::selectedInstanceName() const
@@ -743,13 +863,12 @@ QString NamePage::selectedInstanceName() const
void NamePage::onChanged()
{
m_modified = true;
- updateWarnings();
+ verify();
}
-void NamePage::updateWarnings()
+void NamePage::verify()
{
- const auto root = InstanceManager::singleton().instancesPath();
-
+ const auto root = InstanceManager::singleton().globalInstancesRootPath();
m_okay = checkName(root, ui->instanceName->text());
updateNavigation();
}
@@ -797,14 +916,31 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) :
m_label(ui->pathsLabel),
m_simpleExists(ui->locationExists), m_simpleInvalid(ui->locationInvalid),
m_advancedExists(ui->advancedDirExists),
- m_advancedInvalid(ui->advancedDirInvalid)
+ m_advancedInvalid(ui->advancedDirInvalid),
+ m_okay(false)
{
- QObject::connect(ui->location, &QLineEdit::textEdited, [&]{ onChanged(); });
- QObject::connect(ui->base, &QLineEdit::textEdited, [&]{ onChanged(); });
- QObject::connect(ui->downloads, &QLineEdit::textEdited, [&]{ onChanged(); });
- QObject::connect(ui->mods, &QLineEdit::textEdited, [&]{ onChanged(); });
- QObject::connect(ui->profiles, &QLineEdit::textEdited, [&]{ onChanged(); });
- QObject::connect(ui->overwrite, &QLineEdit::textEdited, [&]{ onChanged(); });
+ auto setEdit = [&](QLineEdit* e) {
+ QObject::connect(e, &QLineEdit::textEdited, [&]{ onChanged(); });
+ QObject::connect(e, &QLineEdit::returnPressed, [&]{ next(); });
+ };
+
+ auto setBrowse = [&](QAbstractButton* b, QLineEdit* e) {
+ QObject::connect(b, &QAbstractButton::clicked, [&]{ browse(e); });
+ };
+
+ setEdit(ui->location);
+ setEdit(ui->base);
+ setEdit(ui->downloads);
+ setEdit(ui->mods);
+ setEdit(ui->profiles);
+ setEdit(ui->overwrite);
+
+ setBrowse(ui->browseLocation, ui->location);
+ setBrowse(ui->browseBase, ui->base);
+ setBrowse(ui->browseDownloads, ui->downloads);
+ setBrowse(ui->browseMods, ui->mods);
+ setBrowse(ui->browseProfiles, ui->profiles);
+ setBrowse(ui->browseOverwrite, ui->overwrite);
QObject::connect(
ui->advancedPathOptions, &QCheckBox::clicked, [&]{ onAdvanced(); });
@@ -812,26 +948,36 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) :
ui->pathPages->setCurrentIndex(0);
}
-
bool PathsPage::ready() const
{
- return checkPaths();
+ // set when the page is activated, textboxes are changed or the advanced
+ // checkbox is toggled
+ return m_okay;
}
-void PathsPage::activated()
+void PathsPage::doActivated(bool firstTime)
{
- const auto name = m_dlg.instanceName();
- const auto type = m_dlg.instanceType();
+ const auto name = m_dlg.rawCreationInfo().instanceName;
+ const auto type = m_dlg.rawCreationInfo().type;
+ // if the instance name or type have changed, all the paths must be
+ // regenerated
const bool changed = (m_lastInstanceName != name) || (m_lastType != type);
+
+ // generating and paths
setPaths(name, changed);
checkPaths();
+
updateNavigation();
- m_label.setText(m_dlg.game()->gameName());
+ m_label.setText(m_dlg.rawCreationInfo().game->gameName());
m_lastInstanceName = name;
m_lastType = type;
+
+ if (firstTime) {
+ ui->location->setFocus();
+ }
}
CreateInstanceDialog::Paths PathsPage::selectedPaths() const
@@ -857,21 +1003,39 @@ void PathsPage::onChanged()
updateNavigation();
}
-bool PathsPage::checkPaths() const
+void PathsPage::browse(QLineEdit* e)
+{
+ const auto s = QFileDialog::getExistingDirectory(&m_dlg, {}, e->text());
+ if (s.isNull() || s.isEmpty()) {
+ return;
+ }
+
+ e->setText(QDir::toNativeSeparators(s));
+}
+
+void PathsPage::checkPaths()
{
if (ui->advancedPathOptions->isChecked()) {
- return
+ // checking advanced paths
+ m_okay =
checkAdvancedPath(ui->base->text()) &&
checkAdvancedPath(resolve(ui->downloads->text())) &&
checkAdvancedPath(resolve(ui->mods->text())) &&
checkAdvancedPath(resolve(ui->profiles->text())) &&
checkAdvancedPath(resolve(ui->overwrite->text()));
} else {
- return checkPath(ui->location->text(), m_simpleExists, m_simpleInvalid);
+ // checking simple path
+ m_okay =
+ checkSimplePath(ui->location->text());
}
}
-bool PathsPage::checkAdvancedPath(const QString& path) const
+bool PathsPage::checkSimplePath(const QString& path)
+{
+ return checkPath(path, m_simpleExists, m_simpleInvalid);
+}
+
+bool PathsPage::checkAdvancedPath(const QString& path)
{
return checkPath(path, m_advancedExists, m_advancedInvalid);
}
@@ -883,6 +1047,9 @@ QString PathsPage::resolve(const QString& path) const
void PathsPage::onAdvanced()
{
+ // the base/location textboxes are different widgets but they represent the
+ // same base path value, so they're synced between pages
+
if (ui->advancedPathOptions->isChecked()) {
ui->base->setText(ui->location->text());
ui->pathPages->setCurrentIndex(1);
@@ -896,19 +1063,21 @@ void PathsPage::onAdvanced()
void PathsPage::setPaths(const QString& name, bool force)
{
- QString path;
+ QString basePath;
- if (m_dlg.instanceType() == CreateInstanceDialog::Portable) {
- path = InstanceManager::portablePath();
+ if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) {
+ basePath = InstanceManager::singleton().portablePath();
} else {
- const auto root = InstanceManager::singleton().instancesPath();
- path = root + "/" + name;
+ const auto root = InstanceManager::singleton().globalInstancesRootPath();
+ basePath = root + "/" + name;
}
- path = QDir::toNativeSeparators(QDir::cleanPath(path));
+ basePath = QDir::toNativeSeparators(QDir::cleanPath(basePath));
+
+ // all paths are set regardless of advanced checkbox
- setIfEmpty(ui->location, path, force);
- setIfEmpty(ui->base, path, force);
+ setIfEmpty(ui->location, basePath, force);
+ setIfEmpty(ui->base, basePath, force);
setIfEmpty(ui->downloads, makeDefaultPath(AppConfig::downloadPath()), force);
setIfEmpty(ui->mods, makeDefaultPath(AppConfig::modsPath()), force);
@@ -925,8 +1094,10 @@ void PathsPage::setIfEmpty(QLineEdit* e, const QString& path, bool force)
bool PathsPage::checkPath(
QString path,
- PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const
+ PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel)
{
+ auto& m = InstanceManager::singleton();
+
bool exists = false;
bool invalid = false;
bool empty = false;
@@ -938,11 +1109,11 @@ bool PathsPage::checkPath(
} else {
const QDir d(path);
- if (InstanceManager::singleton().validInstanceName(d.dirName())) {
- if (m_dlg.instanceType() == CreateInstanceDialog::Portable) {
+ if (m.validInstanceName(d.dirName())) {
+ if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) {
// the default data path for a portable instance is the application
// directory, so it's not an error if it exists
- if (QDir(path) != InstanceManager::singleton().portablePath()) {
+ if (QDir(path) != m.portablePath()) {
exists = QDir(path).exists();
}
} else {
@@ -999,6 +1170,7 @@ NexusPage::~NexusPage() = default;
bool NexusPage::ready() const
{
+ // this page is optional
return true;
}
@@ -1007,71 +1179,56 @@ bool NexusPage::doSkip() const
return m_skip;
}
-void NexusPage::activated()
-{
-}
-
ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg)
: Page(dlg)
{
}
-void ConfirmationPage::activated()
+void ConfirmationPage::doActivated(bool)
{
ui->review->setPlainText(makeReview());
ui->creationLog->clear();
}
-QString ConfirmationPage::toLocalizedString(CreateInstanceDialog::Types t) const
-{
- switch (t)
- {
- case CreateInstanceDialog::Global:
- return QObject::tr("Global");
-
- case CreateInstanceDialog::Portable:
- return QObject::tr("Portable");
-
- default:
- return QObject::tr("Instance type: %1").arg(QObject::tr("?"));
- }
-}
-
QString ConfirmationPage::makeReview() const
{
QStringList lines;
- const auto paths = m_dlg.paths();
- lines.push_back(QObject::tr("Instance type: %1").arg(toLocalizedString(m_dlg.instanceType())));
- lines.push_back(QObject::tr("Instance location: %1").arg(m_dlg.dataPath()));
+ const auto ci = m_dlg.rawCreationInfo();
+
+ lines.push_back(QObject::tr("Instance type: %1")
+ .arg(toLocalizedString(ci.type)));
+
+ lines.push_back(QObject::tr("Instance location: %1")
+ .arg(ci.dataPath));
- if (m_dlg.instanceType() != CreateInstanceDialog::Portable) {
- lines.push_back(QObject::tr("Instance name: %1").arg(m_dlg.instanceName()));
+ if (ci.type != CreateInstanceDialog::Portable) {
+ lines.push_back(QObject::tr("Instance name: %1").arg(ci.instanceName));
}
- if (paths.downloads.isEmpty()) {
+ if (ci.paths.downloads.isEmpty()) {
// simple settings
- if (paths.base != m_dlg.dataPath()) {
- lines.push_back(QObject::tr("Base directory: %1").arg(paths.base));
+ if (ci.paths.base != ci.dataPath) {
+ lines.push_back(QObject::tr("Base directory: %1").arg(ci.paths.base));
}
} else {
// advanced settings
- lines.push_back(QObject::tr("Base directory: %1").arg(paths.base));
- lines.push_back(dirLine(QObject::tr("Downloads"), paths.downloads));
- lines.push_back(dirLine(QObject::tr("Mods"), paths.mods));
- lines.push_back(dirLine(QObject::tr("Profiles"), paths.profiles));
- lines.push_back(dirLine(QObject::tr("Overwrite"), paths.overwrite));
+ lines.push_back(QObject::tr("Base directory: %1").arg(ci.paths.base));
+ lines.push_back(dirLine(QObject::tr("Downloads"), ci.paths.downloads));
+ lines.push_back(dirLine(QObject::tr("Mods"), ci.paths.mods));
+ lines.push_back(dirLine(QObject::tr("Profiles"), ci.paths.profiles));
+ lines.push_back(dirLine(QObject::tr("Overwrite"), ci.paths.overwrite));
}
// game
- QString name = m_dlg.game()->gameName();
- if (!m_dlg.gameVariant().isEmpty()) {
- name += " (" + m_dlg.gameVariant() + ")";
+ QString name = ci.game->gameName();
+ if (!ci.gameVariant.isEmpty()) {
+ name += " (" + ci.gameVariant + ")";
}
lines.push_back(QObject::tr("Game: %1").arg(name));
- lines.push_back(QObject::tr("Game location: %1").arg(m_dlg.gameLocation()));
+ lines.push_back(QObject::tr("Game location: %1").arg(ci.gameLocation));
return lines.join("\n");
}
diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h
index e239c196..08953a74 100644
--- a/src/createinstancedialogpages.h
+++ b/src/createinstancedialogpages.h
@@ -15,14 +15,26 @@ class NexusConnectionUI;
namespace cid
{
+// returns "%base_dir%/dir"
+//
QString makeDefaultPath(const std::wstring& dir);
+// remembers the original text of the given label and, if it contains a %1,
+// sets it in setText()
+//
class PlaceholderLabel
{
public:
PlaceholderLabel(QLabel* label);
+
+ // if the original label text contained a %1, replaces it by the arg and
+ // sets that as the new label text
+ //
void setText(const QString& arg);
+
+ // whether the label is visible
+ //
void setVisible(bool b);
private:
@@ -31,25 +43,73 @@ private:
};
+// one page in the wizard
+//
+// each page can implement one or more selected*() below; those are called
+// by CreateInstanceDialog to gather data from all pages
+//
class Page
{
public:
Page(CreateInstanceDialog& dlg);
+ // whether this page has been filled and is valid; used by the dialog to
+ // determine if it can move to the next page
+ //
virtual bool ready() const;
- virtual void activated();
+ // called every time a page is shown in the screen
+ //
+ void activated();
+
+ // overrides whether this page should be skipped; this is used by
+ // CreateInstanceDialog::setSinglePage() to disable all other pages
+ //
void setSkip(bool b);
+
+ // whether this page should be skipped
+ //
bool skip() const;
+ // asks the dialog to update its navigation buttons, typically used when a
+ // page changes its ready state without moving to a different page
+ //
void updateNavigation();
+
+ // asks the dialog to move to the next page; some pages will automatically
+ // advance once the user has made the proper selection
+ //
void next();
+
+ // called from the dialog when an action is requested on the current page;
+ // returns true when handled
+ //
+ virtual bool action(CreateInstanceDialog::Actions a);
+
+
+ // returns the instance type
+ //
virtual CreateInstanceDialog::Types selectedInstanceType() const;
+
+ // returns the game plugin
+ //
virtual MOBase::IPluginGame* selectedGame() const;
+
+ // returns the game directory
+ //
virtual QString selectedGameLocation() const;
- virtual QString selectedGameVariant() const;
+
+ // returns the game variant
+ //
+ virtual QString selectedGameVariant(MOBase::IPluginGame* game) const;
+
+ // returns the instance name
+ //
virtual QString selectedInstanceName() const;
+
+ // returns the various paths
+ //
virtual CreateInstanceDialog::Paths selectedPaths() const;
protected:
@@ -57,11 +117,22 @@ protected:
CreateInstanceDialog& m_dlg;
const PluginContainer& m_pc;
bool m_skip;
+ bool m_firstActivation;
+
+
+ // called every time a page is shown in the screen; `firstTime` is true for
+ // first activation
+ //
+ virtual void doActivated(bool firstTime);
+ // implemented by derived classes, overridden by setSkip(true)
+ //
virtual bool doSkip() const;
};
+// introduction page, can be disabled by a global setting
+//
class IntroPage : public Page
{
public:
@@ -69,179 +140,480 @@ public:
protected:
bool doSkip() const override;
+
+private:
+ // the setting is only checked once when opening the dialog, or going forwards
+ // then back after checking the box wouldn't show the intro page any more,
+ // which would be unexpected
+ bool m_skip;
};
+// instance type page
+//
class TypePage : public Page
{
public:
TypePage(CreateInstanceDialog& dlg);
+ // whether a type has been been selected
+ //
bool ready() const override;
+
+ // returns the selected type
+ //
CreateInstanceDialog::Types selectedInstanceType() const override;
+ // selects a global instance
+ //
void global();
+
+ // selects a portable instance
+ //
void portable();
+protected:
+ // focuses global instance button on first activation
+ //
+ void doActivated(bool firstTime) override;
+
private:
CreateInstanceDialog::Types m_type;
};
+// game plugin page, displays a list of command buttons for each game, along
+// with a "browse" button for custom directories and filtering stuff
+//
+// the game list initially only shows plugins that report isInstalled(), and the
+// user has two ways of specifying paths for games that were not found:
+//
+// 1) by clicking the "Browse..." button and selecting an arbitrary directory
+//
+// all plugins are checked until one returns true for looksValid(); if none
+// of them do, this is an error
+//
+// 2) by checking the "Show all supported games" checkbox and clicking one
+// of the games on the list
+//
+// if the selected plugin doesn't recognize the directory, the user is
+// warned, but is allowed to continue; there's also some logic to try to
+// find another plugin that can manage this directory and suggest it
+// instead
+//
class GamePage : public Page
{
public:
GamePage(CreateInstanceDialog& dlg);
+ // whether a game has been selected
+ //
bool ready() const override;
+
+ // handles find
+ //
+ bool action(CreateInstanceDialog::Actions a) override;
+
+ // returns the selected game
+ //
MOBase::IPluginGame* selectedGame() const override;
+
+ // returns the selected game directory
QString selectedGameLocation() const override;
+
+ // selects the given game and toggles its associated button; the game
+ // directory can be overridden
+ //
+ // pops up a directory selection dialog if `dir` is empty and the plugin
+ // hasn't detected the game
+ //
void select(MOBase::IPluginGame* game, const QString& dir={});
+
+ // pops up a directory selection dialog and looks for a plugin to manage
+ // it
+ //
void selectCustom();
+ // pops up a warning dialog that the game at the given path is not supported
+ // by any plugin, includes a list of all game plugins in the details section
+ // of the dialog
+ //
void warnUnrecognized(const QString& path);
private:
+ // a single game, with its button and custom directory, if any
+ //
struct Game
{
+ // game plugin
MOBase::IPluginGame* game = nullptr;
+
+ // button on the ui
QCommandLinkButton* button = nullptr;
+
+ // game directory; set in ctor if the plugin has detected the game, or
+ // set later when the user selects a directory
QString dir;
+
+ // whether a directory has been set for this game, either auto detected
+ // or by the user
bool installed = false;
+
Game(MOBase::IPluginGame* g);
Game(const Game&) = delete;
Game& operator=(const Game&) = delete;
};
+ // list of all game plugins, even if they're not installed; those are filtered
+ // from the ui if the checkbox isn't checked
std::vector<std::unique_ptr<Game>> m_games;
+
+ // current selection
Game* m_selection;
+
+ // filter
MOBase::FilterWidget m_filter;
+
+ // returns a list of all the game plugins sorted with natsort
+ //
std::vector<MOBase::IPluginGame*> sortedGamePlugins() const;
- Game* findGame(MOBase::IPluginGame* game);
+
+ // creates the m_games list
+ //
void createGames();
+
+ // finds the game struct associated with the given game
+ //
+ Game* findGame(MOBase::IPluginGame* game);
+
+ // creates the ui for the given game button
+ //
+ void createGameButton(Game* g);
+
+ // adds the given button to the ui
+ //
+ void addButton(QAbstractButton* b);
+
+ // updates the given button on the ui, sets the text, icon, etc.
+ //
void updateButton(Game* g);
+
+ // game buttons are toggles, this creates the button for the given game if
+ // it doesn't exist and toggles it on
+ //
+ // the button might not exist if, for example:
+ // 1) this game is currently filtered out (not installed, doesn't match
+ // filter text, etc) and,
+ // 2) the user browses to a directory that a hidden plugin can use
+ //
void selectButton(Game* g);
+
+ // removes all buttons from the ui
+ //
void clearButtons();
- void addButton(QAbstractButton* b);
+
+ // creates the "Browse" button
+ //
QCommandLinkButton* createCustomButton();
- void createGameButton(Game* g);
+
+
+ // clears the button list and adds all the buttons to it, depending on
+ // filtering and stuff
+ //
void fillList();
- void onFilter();
+
+ // checks whether the given path looks valid to the given game plugin
+ //
+ // if the plugin doesn't like the path, allows the user to override and
+ // accept, but also attempts to find another plugin that wants it and
+ // propose that as an alternative, if there's one
+ //
+ // returns:
+ // - if the user selects the alternative plugin, returns that plugin
+ // instead;
+ // - if the path is bad but the user overrides, returns the given plugin
+ // - if the user cancels or if no plugins can manage the directory, returns
+ // null
+ //
Game* checkInstallation(const QString& path, Game* g);
- MOBase::IPluginGame* findAnotherGame(const QString& path);
+
+ // tells the user that the path cannot be handled by any game plugin, returns
+ // true if the user decides to accept anyway
+ //
bool confirmUnknown(const QString& path, MOBase::IPluginGame* game);
+
+ // tells the user that the path can be handled by a different plugin than the
+ // selected one and allows them to either
+ // 1) use the alternative, guessedGame is returned;
+ // 2) use the selection anyway, selectedGame is returned; or
+ // 3) cancel, null is returned
+ //
MOBase::IPluginGame* confirmOtherGame(
const QString& path,
MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame);
};
+// game variants page; displays a list of command buttons for game variants, as
+// reported by the game plugin
+//
+// this page is always skipped if the game plugin reports no variants
+//
class VariantsPage : public Page
{
public:
VariantsPage(CreateInstanceDialog& dlg);
+ // whether a variant has been selected or the game plugin reports no variants
+ //
bool ready() const override;
- void activated() override;
- QString selectedGameVariant() const override;
+ // uses the game selected in the previous page to fill the list, this must be
+ // called every time because the user may go back in forth in the wizard
+ //
+ void doActivated(bool firstTime) override;
+
+ // returns the selected variant, if any
+ //
+ QString selectedGameVariant(MOBase::IPluginGame* game) const override;
+
+ // selects the given variant
+ //
void select(const QString& variant);
protected:
+ // returns true if the game has no variants
+ //
bool doSkip() const override;
private:
+ // game that was selected the last time this page was active
MOBase::IPluginGame* m_previousGame;
+
+ // buttons
std::vector<QCommandLinkButton*> m_buttons;
+
+ // selected variant
QString m_selection;
+
+ // fills the list with buttons
void fillList();
};
+// instance name page; displays a textbox where the user can enter a name and
+// does basic checks to make sure the name is valid and not a duplicate
+//
+// skipped for portable instances
+//
class NamePage : public Page
{
public:
NamePage(CreateInstanceDialog& dlg);
+ // whether a valid name has been entered
+ //
bool ready() const override;
- void activated() override;
+
+ // returns the instance name
+ //
QString selectedInstanceName() const override;
protected:
+ // uses the selected game to generate an instance name
+ //
+ // as long as the user hasn't modified the textbox, this will regenerate a new
+ // instance name every time the selected game changes
+ //
+ void doActivated(bool firstTime) override;
+
+ // returns true for portable instances
+ //
bool doSkip() const override;
private:
- mutable PlaceholderLabel m_label, m_exists, m_invalid;
+ // game label, replaces %1 with the game name
+ PlaceholderLabel m_label;
+
+ // "instance already exists" label, replaces %1 with instance name
+ PlaceholderLabel m_exists;
+
+ // "instance name invalid" label, replaces %1 with instance name
+ PlaceholderLabel m_invalid;
+
+ // whether the user has modified the text, prevents auto generation when the
+ // selected game changes
bool m_modified;
+
+ // whether the instance name is valid
bool m_okay;
+
+ // called when the user modifies the textbox, remember that it has changed and
+ // calls verify()
+ //
void onChanged();
- void updateWarnings();
+
+ // check if the entered name is valid, sets m_okay and calls checkName()
+ //
+ void verify();
+
+ // updates the ui depending on whether the given instance name is valid in
+ // the given directory; returns false if the name is invalid
+ //
bool checkName(QString parentDir, QString name);
};
+// instance paths page; shows a single textbox for the base directory, or a
+// series of textboxes for all the configurable paths if the advanced checkbox
+// is checked
+//
class PathsPage : public Page
{
public:
PathsPage(CreateInstanceDialog& dlg);
+ // whether all paths make sense
+ //
bool ready() const override;
- void activated() override;
+ // returns the selected paths
+ //
CreateInstanceDialog::Paths selectedPaths() const override;
+protected:
+ // resets all the paths if the instance type or instance name have changed,
+ // the current values are kept as long as these don't change; also updates the
+ // game name in the ui
+ //
+ void doActivated(bool firstTime) override;
+
private:
+ // instance name the last time this page was active
QString m_lastInstanceName;
+
+ // instance type the last time this page was active
CreateInstanceDialog::Types m_lastType;
+
+ // help label, replaces %1 by the game name
PlaceholderLabel m_label;
- mutable PlaceholderLabel m_simpleExists, m_simpleInvalid;
- mutable PlaceholderLabel m_advancedExists, m_advancedInvalid;
+ // path exists/is invalid labels for the simple page, replaces %1 with the
+ // path
+ PlaceholderLabel m_simpleExists, m_simpleInvalid;
+
+ // path exists/is invalid labels for the advanced page, replaces %1 with the
+ // path
+ PlaceholderLabel m_advancedExists, m_advancedInvalid;
+
+ // whether the paths are valid
+ bool m_okay;
+
+
+ // called when the user changes any textbox, checks the path and updates nav
+ //
void onChanged();
- bool checkPaths() const;
- bool checkAdvancedPath(const QString& path) const;
+
+ // opens a browse directory dialog and sets the given textbox
+ //
+ void browse(QLineEdit* e);
+
+ // checks the simple or advanced paths, sets m_okay
+ //
+ void checkPaths();
+
+ // checks a simple path, forwards to checkPath() with the simple labels
+ //
+ bool checkSimplePath(const QString& path);
+
+ // checks an advanced path, forwards to checkPath() with the advanced labels
+ //
+ bool checkAdvancedPath(const QString& path);
+
+ // returns false if the path is invalid or already exists, sets the given
+ // labels accordingly
+ //
+ bool checkPath(
+ QString path,
+ PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel);
+
+ // replaces %base_dir% in the given path by whatever's in the base path
+ // textbox
+ //
QString resolve(const QString& path) const;
+
+ // called when the advanced checkbox is toggled, switches the active page
+ // and checks the paths
+ //
void onAdvanced();
+
+ // called whenever the page becomes active
+ //
+ // this normally doesn't change the textboxes unless they're empty, but if the
+ // instance name or type have changed, `force` is true, which forces all paths
+ // to reset
+ //
void setPaths(const QString& name, bool force);
+
+ // sets the given textbox to the path if it's empty or if `force` is true
+ //
void setIfEmpty(QLineEdit* e, const QString& path, bool force);
- bool checkPath(
- QString path,
- PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const;
};
+// nexus connection page; this reuses the ui found in the settings dialog and
+// is skipped if there's already an api key in the credentials manager
+//
class NexusPage : public Page
{
public:
NexusPage(CreateInstanceDialog& dlg);
~NexusPage();
+ // always returns true, this is an optional page
+ //
bool ready() const override;
- void activated() override;
protected:
+ // returns true if the api key was already detected
+ //
bool doSkip() const override;
private:
+ // connection ui
std::unique_ptr<NexusConnectionUI> m_connectionUI;
+
+ // set to true only if the api key was detected when opening the dialog, or
+ // going back and forth would skip the page after the process is completed,
+ // which would be unexpected
bool m_skip;
};
+// shows a text log of all the creation parameters
+//
class ConfirmationPage : public Page
{
public:
ConfirmationPage(CreateInstanceDialog& dlg);
- void activated() override;
+ // recreates the log with the latest settings
+ //
+ void doActivated(bool firstTime) override;
- QString toLocalizedString(CreateInstanceDialog::Types t) const;
+ // returns the text for the log
+ //
QString makeReview() const;
+
+private:
+ // returns a log line with the given caption and path, something like
+ // " - caption: path"
+ //
QString dirLine(const QString& caption, const QString& path) const;
};
diff --git a/src/env.cpp b/src/env.cpp
index 5bed84c1..98d2e6ea 100644
--- a/src/env.cpp
+++ b/src/env.cpp
@@ -4,6 +4,7 @@
#include "envsecurity.h"
#include "envshortcut.h"
#include "envwindows.h"
+#include "envdump.h"
#include "settings.h"
#include <log.h>
#include <utility.h>
@@ -1182,8 +1183,16 @@ HandlePtr tempFile(const std::wstring dir)
return {};
}
-HandlePtr dumpFile()
+HandlePtr dumpFile(const wchar_t* dir)
{
+ // try the given directory, if any
+ if (dir) {
+ HandlePtr h = tempFile(dir);
+ if (h.get() != INVALID_HANDLE_VALUE) {
+ return h;
+ }
+ }
+
// try the current directory
HandlePtr h = tempFile(L".");
if (h.get() != INVALID_HANDLE_VALUE) {
@@ -1193,10 +1202,10 @@ HandlePtr dumpFile()
std::wclog << L"cannot write dump file in current directory\n";
// try the temp directory
- const auto dir = tempDir();
+ const auto temp = tempDir();
- if (!dir.empty()) {
- h = tempFile(dir.c_str());
+ if (!temp.empty()) {
+ h = tempFile(temp.c_str());
if (h.get() != INVALID_HANDLE_VALUE) {
return h;
}
@@ -1235,11 +1244,11 @@ std::string toString(CoreDumpTypes type)
}
-bool createMiniDump(HANDLE process, CoreDumpTypes type)
+bool createMiniDump(const wchar_t* dir, HANDLE process, CoreDumpTypes type)
{
const DWORD pid = GetProcessId(process);
- const HandlePtr file = dumpFile();
+ const HandlePtr file = dumpFile(dir);
if (!file) {
std::wcerr << L"nowhere to write the dump file\n";
return false;
@@ -1278,10 +1287,10 @@ bool createMiniDump(HANDLE process, CoreDumpTypes type)
}
-bool coredump(CoreDumpTypes type)
+bool coredump(const wchar_t* dir, CoreDumpTypes type)
{
std::wclog << L"creating minidump for the current process\n";
- return createMiniDump(GetCurrentProcess(), type);
+ return createMiniDump(dir, GetCurrentProcess(), type);
}
bool coredumpOther(CoreDumpTypes type)
@@ -1309,7 +1318,7 @@ bool coredumpOther(CoreDumpTypes type)
return false;
}
- return createMiniDump(handle.get(), type);
+ return createMiniDump(nullptr, handle.get(), type);
}
} // namespace
diff --git a/src/env.h b/src/env.h
index dbbd5cf4..e9cb7a36 100644
--- a/src/env.h
+++ b/src/env.h
@@ -309,27 +309,6 @@ bool registryValueExists(const QString& key, const QString& value);
//
void deleteRegistryKeyIfEmpty(const QString& name);
-
-enum class CoreDumpTypes
-{
- Mini = 1,
- Data,
- Full
-};
-
-
-CoreDumpTypes coreDumpTypeFromString(const std::string& s);
-std::string toString(CoreDumpTypes type);
-
-// creates a minidump file for this process
-//
-bool coredump(CoreDumpTypes type);
-
-// finds another process with the same name as this one and creates a minidump
-// file for it
-//
-bool coredumpOther(CoreDumpTypes type);
-
} // namespace env
#endif // ENV_ENV_H
diff --git a/src/envdump.h b/src/envdump.h
new file mode 100644
index 00000000..355df783
--- /dev/null
+++ b/src/envdump.h
@@ -0,0 +1,30 @@
+#ifndef MODORGANIZER_ENVDUMP_INCLUDED
+#define MODORGANIZER_ENVDUMP_INCLUDED
+
+namespace env
+{
+
+enum class CoreDumpTypes
+{
+ None,
+ Mini,
+ Data,
+ Full
+};
+
+
+CoreDumpTypes coreDumpTypeFromString(const std::string& s);
+std::string toString(CoreDumpTypes type);
+
+// creates a minidump file for this process
+//
+bool coredump(const wchar_t* dir, CoreDumpTypes type);
+
+// finds another process with the same name as this one and creates a minidump
+// file for it
+//
+bool coredumpOther(CoreDumpTypes type);
+
+} // namespace
+
+#endif // MODORGANIZER_ENVDUMP_INCLUDED
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp
index 8163149b..4191f850 100644
--- a/src/instancemanager.cpp
+++ b/src/instancemanager.cpp
@@ -21,9 +21,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "instancemanager.h"
#include "selectiondialog.h"
#include "settings.h"
-#include "shared/appconfig.h"
#include "plugincontainer.h"
+#include "nexusinterface.h"
+#include "createinstancedialog.h"
+#include "instancemanagerdialog.h"
+#include "createinstancedialogpages.h"
+#include "shared/appconfig.h"
#include "shared/util.h"
+#include "shared/error_report.h"
#include <report.h>
#include <iplugingame.h>
#include <utility.h>
@@ -38,7 +43,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
-Instance::Instance(QDir dir, bool portable, QString profileName) :
+Instance::Instance(QString dir, bool portable, QString profileName) :
m_dir(std::move(dir)), m_portable(portable), m_plugin(nullptr),
m_profile(std::move(profileName))
{
@@ -49,7 +54,7 @@ QString Instance::name() const
if (isPortable())
return QObject::tr("Portable");
else
- return m_dir.dirName();
+ return QDir(m_dir).dirName();
}
QString Instance::gameName() const
@@ -62,11 +67,16 @@ QString Instance::gameDirectory() const
return m_gameDir;
}
-QDir Instance::directory() const
+QString Instance::directory() const
{
return m_dir;
}
+QString Instance::baseDirectory() const
+{
+ return m_baseDir;
+}
+
MOBase::IPluginGame* Instance::gamePlugin() const
{
return m_plugin;
@@ -79,7 +89,7 @@ QString Instance::profileName() const
QString Instance::iniPath() const
{
- return InstanceManager::iniPath(m_dir);
+ return InstanceManager::singleton().iniPath(m_dir);
}
bool Instance::isPortable() const
@@ -87,15 +97,32 @@ bool Instance::isPortable() const
return m_portable;
}
-Instance::SetupResults Instance::setup(PluginContainer& plugins)
+bool Instance::isActive() const
+{
+ auto& m = InstanceManager::singleton();
+
+ if (auto i=m.currentInstance())
+ {
+ if (m_portable) {
+ return i->isPortable();
+ } else {
+ return (i->name() == name());
+ }
+ }
+
+ return false;
+}
+
+bool Instance::readFromIni()
{
Settings s(iniPath());
if (s.iniStatus() != QSettings::NoError) {
log::error("can't read ini {}", iniPath());
- return SetupResults::BadIni;
+ return false;
}
+ // game name and directory are from ini unless overridden by setGame()
if (m_gameName.isEmpty()) {
if (auto v=s.game().name())
m_gameName = *v;
@@ -106,35 +133,71 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins)
m_gameDir = *v;
}
- const auto r = getGamePlugin(plugins);
- if (r != SetupResults::Ok) {
- return r;
- }
-
if (m_gameVariant.isEmpty()) {
if (auto v=s.game().edition()) {
m_gameVariant = *v;
}
}
+ if (m_baseDir.isEmpty()) {
+ m_baseDir = s.paths().base();
+ }
+
+ // figuring out profile from ini if it's missing
+ getProfile(s);
+
+ return true;
+}
+
+Instance::SetupResults Instance::setup(PluginContainer& plugins)
+{
+ // read initial values from the ini
+ if (!readFromIni()) {
+ return SetupResults::BadIni;
+ }
+
+ // getting game plugin
+ const auto r = getGamePlugin(plugins);
+ if (r != SetupResults::Okay) {
+ return r;
+ }
+
+ // error if the variant missing and required by the plugin
if (m_gameVariant.isEmpty() && m_plugin->gameVariants().size() > 1) {
return SetupResults::MissingVariant;
} else {
m_plugin->setGameVariant(m_gameVariant);
}
- getProfile(s);
+ // update the ini in case anything was missing
+ updateIni();
+
+ // the game directory may be different than what the plugin detected, the user
+ // can change it in the settings and might have multiple versions of the game
+ // installed
+ m_plugin->setGamePath(m_gameDir);
+
+ return SetupResults::Okay;
+}
+void Instance::updateIni()
+{
+ Settings s(iniPath());
+
+ if (s.iniStatus() != QSettings::NoError) {
+ log::error("can't open ini {}", iniPath());
+ return;
+ }
+
+ // updating the settings since some of these values might have been missing
s.game().setName(m_gameName);
s.game().setDirectory(m_gameDir);
s.game().setSelectedProfileName(m_profile);
- if (!m_gameVariant.isEmpty())
+ if (!m_gameVariant.isEmpty()) {
+ // don't write a variant to the ini if the plugin doesn't require one
s.game().setEdition(m_gameVariant);
-
- m_plugin->setGamePath(m_gameDir);
-
- return SetupResults::Ok;
+ }
}
void Instance::setGame(const QString& name, const QString& dir)
@@ -185,7 +248,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins)
}
m_plugin = game;
- return SetupResults::Ok;
+ return SetupResults::Okay;
}
}
@@ -209,7 +272,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins)
m_plugin = game;
m_gameName = game->gameName();
- return SetupResults::Ok;
+ return SetupResults::Okay;
}
}
@@ -238,7 +301,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins)
m_plugin = game;
m_gameDir = game->gameDirectory().absolutePath();
- return SetupResults::Ok;
+ return SetupResults::Okay;
} else {
log::warn(
"found plugin {} that matches name in ini {}, but no game install "
@@ -262,7 +325,6 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins)
}
}
-
void Instance::getProfile(const Settings& s)
{
if (!m_profile.isEmpty()) {
@@ -284,6 +346,176 @@ void Instance::getProfile(const Settings& s)
iniPath(), m_profile);
}
+// returns a list of files and folders that must be deleted when deleting
+// this instance
+//
+std::vector<Instance::Object> Instance::objectsForDeletion() const
+{
+ // native separators and ending slash
+ auto prettyDir = [](auto s) {
+ if (!s.endsWith("/") || !s.endsWith("\\")) {
+ s += "/";
+ }
+
+ return QDir::toNativeSeparators(s);
+ };
+
+ // native separators
+ auto prettyFile = [](auto s) {
+ return QDir::toNativeSeparators(s);
+ };
+
+
+ // lowercase, native separators and ending slash
+ auto canonicalDir = [](QString s) {
+ s = s.toLower();
+
+ if (!s.endsWith("/") || !s.endsWith("\\")) {
+ s += "/";
+ }
+
+ return QDir::toNativeSeparators(s);
+ };
+
+ // lower and native separators
+ auto canonicalFile = [](auto s) {
+ return QDir::toNativeSeparators(s.toLower());
+ };
+
+
+
+ // whether the given directory is contained in the root
+ auto dirInRoot = [&](const QString& root, const QString& dir) {
+ return canonicalDir(dir).startsWith(canonicalDir(root));
+ };
+
+ // whether the given file is contained in the root
+ auto fileInRoot = [&](const QString& root, const QString& file) {
+ return canonicalFile(file).startsWith(canonicalDir(root));
+ };
+
+
+ Settings settings(iniPath());
+
+ if (settings.iniStatus() != QSettings::NoError) {
+ log::error("can't read ini {}", iniPath());
+ return {};
+ }
+
+
+
+ const QString loc = directory();
+ const QString base = settings.paths().base();
+
+
+ // directories that might contain the individual files and directories
+ // set in the path settings
+ std::vector<Object> roots;
+
+ // a portable instance has its location in the installation directory,
+ // don't delete that
+ if (!isPortable()) {
+ if (QDir(loc).exists()) {
+ roots.push_back({loc, true});
+ }
+ }
+
+ // the base directory is the location directory by default, don't add it
+ // if it's the same
+ if (canonicalDir(base) != canonicalDir(loc)) {
+ if (QDir(base).exists()) {
+ roots.push_back({base, false});
+ }
+ }
+
+
+ // all the directories that are part of an instance; none of them are
+ // mandatory for deletion
+ const std::vector<Object> dirs = {
+ settings.paths().downloads(),
+ settings.paths().mods(),
+ settings.paths().cache(),
+ settings.paths().profiles(),
+ settings.paths().overwrite(),
+ QDir(m_dir).filePath(QString::fromStdWString(AppConfig::dumpsDir())),
+ QDir(m_dir).filePath(QString::fromStdWString(AppConfig::logPath())),
+ };
+
+ // all the files that are part of an instance
+ const std::vector<Object> files = {
+ {iniPath(), true}, // the ini file must be deleted
+ };
+
+
+ // this will contain the root directories, plus all the individual
+ // directories that are not inside these roots
+ std::vector<Object> cleanDirs;
+
+ for (const auto& f : dirs) {
+ bool inRoots = false;
+
+ for (const auto& root : roots) {
+ if (dirInRoot(root.path, f.path)) {
+ inRoots = true;
+ break;
+ }
+ }
+
+ if (!inRoots) {
+ // not in roots, this is a path that was changed by the user; make
+ // sure it exists
+ if (QDir(f.path).exists()) {
+ cleanDirs.push_back({prettyDir(f.path), f.mandatoryDelete});
+ }
+ }
+ }
+
+ // prepending the roots
+ for (auto itor=roots.rbegin(); itor!=roots.rend(); ++itor) {
+ cleanDirs.insert(
+ cleanDirs.begin(),
+ {prettyDir(itor->path), itor->mandatoryDelete});
+ }
+
+
+
+ // this will contain the individual files that are not inside the roots;
+ // not that this only contains the INI file for now, so most of this is
+ // useless
+ std::vector<Object> cleanFiles;
+
+ for (const auto& f : files) {
+ bool inRoots = false;
+
+ for (const auto& root : roots) {
+ if (fileInRoot(root.path, f.path)) {
+ inRoots = true;
+ break;
+ }
+ }
+
+ if (!inRoots) {
+ // not in roots, this is a path that was changed by the user; make
+ // sure it exists
+ if (QFileInfo(f.path).exists()) {
+ cleanFiles.push_back({prettyFile(f.path), f.mandatoryDelete});
+ }
+ }
+ }
+
+
+ // contains all the directories and files to be deleted
+ std::vector<Object> all;
+ all.insert(all.end(), cleanDirs.begin(), cleanDirs.end());
+ all.insert(all.end(), cleanFiles.begin(), cleanFiles.end());
+
+ // mandatory on top
+ std::stable_sort(all.begin(), all.end());
+
+ return all;
+}
+
+
InstanceManager::InstanceManager()
{
@@ -299,48 +531,44 @@ InstanceManager &InstanceManager::singleton()
void InstanceManager::overrideInstance(const QString& instanceName)
{
m_overrideInstanceName = instanceName;
- m_overrideInstance = true;
}
void InstanceManager::overrideProfile(const QString& profileName)
{
m_overrideProfileName = profileName;
- m_overrideProfile = true;
}
std::optional<Instance> InstanceManager::currentInstance() const
{
- const QString profile = m_overrideProfile ? m_overrideProfileName : "";
+ const QString profile = m_overrideProfileName ?
+ *m_overrideProfileName : "";
- if (portableInstallIsLocked()) {
- // force portable instance
- return Instance(QDir(portablePath()), true, profile);
- }
+ const QString name = m_overrideInstanceName ?
+ *m_overrideInstanceName : GlobalSettings::currentInstance();
- QString name;
- if (m_overrideInstance)
- name = m_overrideInstanceName;
- else
- name = GlobalSettings::currentInstance();
+ if (!allowedToChangeInstance()) {
+ // force portable instance
+ return Instance(portablePath(), true, profile);
+ }
if (name.isEmpty()) {
if (portableInstanceExists()) {
// use portable
- return Instance(QDir(portablePath()), true, profile);
+ return Instance(portablePath(), true, profile);
} else {
// no instance set
return {};
}
}
- return Instance(QDir(instancePath(name)), false, profile);
+ return Instance(instancePath(name), false, profile);
}
void InstanceManager::clearCurrentInstance()
{
setCurrentInstance("");
- m_overrideInstance = false;
+ m_overrideInstanceName = {};
}
void InstanceManager::setCurrentInstance(const QString &name)
@@ -350,30 +578,31 @@ void InstanceManager::setCurrentInstance(const QString &name)
QString InstanceManager::instancePath(const QString& instanceName) const
{
- return QDir::fromNativeSeparators(instancesPath() + "/" + instanceName);
+ return QDir::fromNativeSeparators(globalInstancesRootPath() + "/" + instanceName);
}
-QString InstanceManager::instancesPath() const
+QString InstanceManager::globalInstancesRootPath() const
{
return QDir::fromNativeSeparators(
QStandardPaths::writableLocation(QStandardPaths::DataLocation));
}
-QString InstanceManager::iniPath(const QDir& instanceDir)
+QString InstanceManager::iniPath(const QString& instanceDir) const
{
- return instanceDir.filePath(QString::fromStdWString(AppConfig::iniFileName()));
+ return QDir(instanceDir).filePath(
+ QString::fromStdWString(AppConfig::iniFileName()));
}
-std::vector<QDir> InstanceManager::instancePaths() const
+std::vector<QString> InstanceManager::globalInstancePaths() const
{
const std::set<QString> ignore = {
"cache", "qtwebengine",
};
- const QDir root(instancesPath());
+ const QDir root(globalInstancesRootPath());
const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
- std::vector<QDir> list;
+ std::vector<QString> list;
for (auto&& d : dirs) {
if (!ignore.contains(QFileInfo(d).fileName().toLower())) {
@@ -384,24 +613,12 @@ std::vector<QDir> InstanceManager::instancePaths() const
return list;
}
-QStringList InstanceManager::instanceNames() const
-{
- QStringList list;
-
- for (auto&& d : instancePaths()) {
- list.push_back(d.dirName());
- }
-
- return list;
-}
-
-
-bool InstanceManager::isPortablePath(const QString& dataPath)
+bool InstanceManager::hasAnyInstances() const
{
- return (dataPath == portablePath());
+ return portableInstanceExists() || !globalInstancePaths().empty();
}
-QString InstanceManager::portablePath()
+QString InstanceManager::portablePath() const
{
return qApp->applicationDirPath();
}
@@ -412,23 +629,21 @@ bool InstanceManager::portableInstanceExists() const
QString::fromStdWString(AppConfig::iniFileName()));
}
-
-bool InstanceManager::portableInstallIsLocked() const
-{
- return QFile::exists(qApp->applicationDirPath() + "/" +
- QString::fromStdWString(AppConfig::portableLockFileName()));
-}
-
bool InstanceManager::allowedToChangeInstance() const
{
- return !portableInstallIsLocked();
+ const auto lockFile =
+ qApp->applicationDirPath() + "/" +
+ QString::fromStdWString(AppConfig::portableLockFileName());
+
+ return !QFile::exists(lockFile);
}
const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
- const QDir& instanceDir, const PluginContainer& plugins) const
+ const QString& instanceDir, const PluginContainer& plugins) const
{
const QString ini = iniPath(instanceDir);
+ // reading ini
Settings s(ini);
if (s.iniStatus() != QSettings::NoError)
@@ -437,6 +652,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
return nullptr;
}
+ // using game name from ini, if available
const auto instanceGameName = s.game().name();
if (instanceGameName && !instanceGameName->isEmpty())
@@ -448,7 +664,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
}
log::error(
- "no plugin reports game name '{}' found in ini {}",
+ "no plugin has game name '{}' that was found in ini {}",
*instanceGameName, ini);
}
else
@@ -457,8 +673,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
}
- log::error("falling back on looksValid check");
-
+ // using game directory from ini, if available
const auto gameDir = s.game().directory();
if (gameDir && !gameDir->isEmpty())
@@ -470,7 +685,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
}
log::error(
- "no plugins appear to support game directory '{}' from ini {}",
+ "no plugin appears to support game directory '{}' from ini {}",
*gameDir, ini);
}
else
@@ -478,6 +693,17 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
log::error("no game directory found in ini {}", ini);
}
+
+ // looking for a plugin that can handle the directory
+ log::debug("falling back on looksValid check");
+
+ for (const IPluginGame* game : plugins.plugins<IPluginGame>()) {
+ if (game->looksValid(instanceDir)) {
+ return game;
+ }
+ }
+
+
return nullptr;
}
@@ -485,6 +711,7 @@ QString InstanceManager::makeUniqueName(const QString& instanceName) const
{
const QString sanitized = sanitizeInstanceName(instanceName);
+ // trying "name (N)"
QString name = sanitized;
for (int i=2; i<100; ++i) {
if (!instanceExists(name)) {
@@ -499,7 +726,7 @@ QString InstanceManager::makeUniqueName(const QString& instanceName) const
bool InstanceManager::instanceExists(const QString& instanceName) const
{
- const QDir root = instancesPath();
+ const QDir root = globalInstancesRootPath();
return root.exists(instanceName);
}
@@ -529,3 +756,198 @@ bool InstanceManager::validInstanceName(const QString& instanceName) const
return (instanceName == sanitizeInstanceName(instanceName));
}
+
+
+
+std::optional<Instance> selectInstance()
+{
+ auto& m = InstanceManager::singleton();
+
+ // since there is no instance currently active, load plugins with a null
+ // OrganizerCore; see PluginContainer::initPlugin()
+ NexusInterface ni(nullptr);
+ PluginContainer pc(nullptr);
+ pc.loadPlugins();
+
+ if (m.hasAnyInstances()) {
+ // there is at least one instance available, show the instance manager
+ // dialog
+ InstanceManagerDialog dlg(pc);
+
+ // the dialog normally restarts MO when an instance is selected, but this
+ // is not necessary here since MO hasn't really started yet
+ dlg.setRestartOnSelect(false);
+
+ dlg.show();
+ dlg.activateWindow();
+ dlg.raise();
+
+ if (dlg.exec() != QDialog::Accepted) {
+ return {};
+ }
+
+ } else {
+ // no instances configured, ask the user to create one
+ CreateInstanceDialog dlg(pc, nullptr);
+
+ if (dlg.exec() != QDialog::Accepted) {
+ return {};
+ }
+ }
+
+ // return the new instance or the selection
+ return m.currentInstance();
+}
+
+// shows the game selection page of the create instance dialog so the user can
+// pick which game is managed by this instance
+//
+// this is used below in setupInstance() when the game directory is gone or
+// no plugins can recognize it
+//
+SetupInstanceResults selectGame(Instance& instance, PluginContainer& pc)
+{
+ CreateInstanceDialog dlg(pc, nullptr);
+
+ // only show the game page
+ dlg.setSinglePage<cid::GamePage>(instance.name());
+
+ dlg.show();
+ dlg.activateWindow();
+ dlg.raise();
+
+ if (dlg.exec() != QDialog::Accepted) {
+ // cancelled
+ return SetupInstanceResults::Exit;
+ }
+
+ // this info will be used instead of the ini, which should fix this
+ // particular problem
+ instance.setGame(
+ dlg.creationInfo().game->gameName(),
+ dlg.creationInfo().gameLocation);
+
+ return SetupInstanceResults::TryAgain;
+}
+
+// shows the game variant page of the create instance dialog so the user can
+// pick which game variant is installed
+//
+// this is used below in setupInstance() when there is no variant in the ini
+// but the game plugin requires one; this can happen when the ini is broken
+// or when a new variant has become supported by the plugin for a game the
+// user already has an instance for
+//
+SetupInstanceResults selectVariant(Instance& instance, PluginContainer& pc)
+{
+ CreateInstanceDialog dlg(pc, nullptr);
+
+ // the variant page uses the game page to know which game was selected, so
+ // set it manually
+ dlg.getPage<cid::GamePage>()->select(
+ instance.gamePlugin(), instance.gameDirectory());
+
+ // only show the variant page
+ dlg.setSinglePage<cid::VariantsPage>(instance.name());
+
+ dlg.show();
+ dlg.activateWindow();
+ dlg.raise();
+
+ if (dlg.exec() != QDialog::Accepted) {
+ return SetupInstanceResults::Exit;
+ }
+
+ // this info will be used instead of the ini, which should fix this
+ // particular problem
+ instance.setVariant(dlg.creationInfo().gameVariant);
+
+ return SetupInstanceResults::TryAgain;
+}
+
+SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc)
+{
+ // set up the instance
+ const auto setupResult = instance.setup(pc);
+
+ switch (setupResult)
+ {
+ case Instance::SetupResults::Okay:
+ {
+ // all good
+ return SetupInstanceResults::Okay;
+ }
+
+ case Instance::SetupResults::BadIni:
+ {
+ // unreadable ini, there's not much that can be done, select another
+ // instance
+
+ MOShared::criticalOnTop(
+ QObject::tr("Cannot open instance '%1', failed to read INI file %2.")
+ .arg(instance.name()).arg(instance.iniPath()));
+
+ return SetupInstanceResults::SelectAnother;
+ }
+
+ case Instance::SetupResults::IniMissingGame:
+ {
+ // both the game name and directory are missing from the ini; although
+ // this shouldn't happen, setup() is able to handle when either is
+ // missing, but not both
+ //
+ // ask the user for the game managed by this instance
+
+ MOShared::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()));
+
+ return selectGame(instance, pc);
+ }
+
+ case Instance::SetupResults::PluginGone:
+ {
+ // there is no plugin that can handle the game name/directory from the
+ // ini, so this instance is unusable
+
+ MOShared::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()));
+
+ return SetupInstanceResults::SelectAnother;
+ }
+
+ case Instance::SetupResults::GameGone:
+ {
+ // the game directory doesn't exist or the plugin doesn't recognize it;
+ // ask the user for the game managed by this instance
+
+ MOShared::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()));
+
+ return selectGame(instance, pc);
+ }
+
+ case Instance::SetupResults::MissingVariant:
+ {
+ // the game variant is missing from the ini, ask the user for it
+ return selectVariant(instance, pc);
+ }
+
+ default:
+ {
+ // shouldn't happen
+ return SetupInstanceResults::Exit;
+ }
+ }
+}
diff --git a/src/instancemanager.h b/src/instancemanager.h
index 79f1f30b..4f8b01c7 100644
--- a/src/instancemanager.h
+++ b/src/instancemanager.h
@@ -10,87 +10,363 @@ class Settings;
class PluginContainer;
+// represents an instance, either global or portable
+//
+// if setup() is not called, the game plugin is not available and the INI is
+// not processed at all, so name(), directory() and isPortable() really are the
+// only meaningful functions
+//
+// setup() must be called when MO wants to use the instance, it will read the
+// INI, figure out the game plugin to use and set it up by calling
+// setGameVaraint(), setGamePath(), etc. on it
+//
+// when setup() fails because the game name/directory or variant are missing,
+// setGame() and setVariant() can be called before retrying setup(); this
+// happens on startup if that information is missing
+//
class Instance
{
public:
+ // returned by setup()
+ //
enum class SetupResults
{
- Ok,
+ // instance is ready to be used
+ Okay,
+
+ // error while reading the INI
BadIni,
+
+ // both the game name and directory are missing from the ini; setup() will
+ // attempt to recover if either are missing, but not when both are
IniMissingGame,
+
+ // either:
+ // 1) there is no plugin with the given name, or
+ // 2) if the name is missing, no plugin can handle the game directory
PluginGone,
+
+ // the selected plugin does not consider the game directory as being valid
GameGone,
+
+ // there is no game variant specified in the INI, but the plugin requires
+ // one
MissingVariant
};
- Instance(QDir dir, bool portable, QString profileName={});
+ // a file or directory owned by this instance, used by objectsForDeletion()
+ //
+ struct Object
+ {
+ // path to the file or directory
+ QString path;
+
+ // whether this object must be deleted to properly delete the instance;
+ // typically true only for the instance directory itself, but not for the
+ // base directory, etc.
+ bool mandatoryDelete;
+
+
+ Object(QString p, bool d=false)
+ : path(std::move(p)), mandatoryDelete(d)
+ {
+ }
+
+ // puts mandatory delete on top
+ //
+ bool operator<(const Object& o) const
+ {
+ if (mandatoryDelete && !o.mandatoryDelete) {
+ return true;
+ } else if (!mandatoryDelete && o.mandatoryDelete) {
+ return false;
+ }
+
+ return false;
+ }
+ };
+
+
+
+ // an instance that lives in the given directory; `portable` must be `true`
+ // if this is a portable instance
+ //
+ // `profileName` can be given to override what's in the INI; this typically
+ // happens when the profile is overriden on the command line
+ //
+ Instance(QString dir, bool portable, QString profileName={});
+
+
+ // reads in values from the INI if they were not given yet:
+ // - game name
+ // - game directory
+ // - game variant
+ // - profile name
+ //
+ // note that setup() already calls this
+ //
+ // returns false if the ini couldn't be read from
+ //
+ bool readFromIni();
+
+
+ // finds the appropriate game plugin and sets it up so MO can use it; this
+ // calls readFromIni() first
+ //
+ // setup() tries to recover from some errors, but can fail for a variety of
+ // reasons, see SetupResults
+ //
SetupResults setup(PluginContainer& plugins);
+
+ // overrides the game name and directory
+ //
void setGame(const QString& name, const QString& dir);
+
+ // overrides the game variant
+ //
void setVariant(const QString& name);
+
+ // returns the instance name; this is the directory name or "Portable" for
+ // portable instances
+ //
+ // can be called without setup()
+ //
QString name() const;
+
+ // returns either:
+ // 1) the game name from the INI, if readFromIni() was called;
+ // 2) gameName() from the game plugin if it was missing and setup() was
+ // called; or
+ // 3) whatever was given in setGame()
+ //
QString gameName() const;
+
+ // returns either:
+ // 1) the game directory from the INI, if readFromIni() was called;
+ // 2) gameDirectory() from the game plugin if it was missing and setup()
+ // was called; or
+ // 3) whatever was given in setGame()
+ //
QString gameDirectory() const;
- QDir directory() const;
+
+ // returns the instance directory as given in the constructor
+ //
+ QString directory() const;
+
+ // returns the base directory; empty if readFromIni() hasn't been called
+ //
+ QString baseDirectory() const;
+
+ // returns whether this is a portable instance, as given in the constructor
+ //
+ bool isPortable() const;
+
+ // returns the selected game plugin; will return null if setup() hasn't been
+ // called, or if it failed
+ //
MOBase::IPluginGame* gamePlugin() const;
+
+ // returns either:
+ // 1) the profile name given in the constructor if not empty;
+ // 2) the profile name from the INI if readFromIni() was called, or
+ // 3) the default profile name if it's missing (see
+ // AppConfig::defaultProfileName())
+ //
QString profileName() const;
+
+ // returns the path to the INI file for this instance; the file may not
+ // exist
+ //
QString iniPath() const;
- bool isPortable() const;
+
+ // whether this is the currently active instance in MO
+ //
+ bool isActive() const;
+
+ // returns a list of files and directories that must be deleted when deleting
+ // this instance; this will read the INI and fail if it's not accessible
+ //
+ // returns an empty list on failure
+ //
+ std::vector<Object> objectsForDeletion() const;
private:
- QDir m_dir;
+ QString m_dir;
bool m_portable;
- QString m_gameName, m_gameDir, m_gameVariant;
+ QString m_gameName, m_gameDir, m_gameVariant, m_baseDir;
MOBase::IPluginGame* m_plugin;
QString m_profile;
+ // figures out the game plugin for this instance
+ //
SetupResults getGamePlugin(PluginContainer& plugins);
+
+ // figures out the profile name for this instance
+ //
void getProfile(const Settings& s);
+
+ // updates the ini with the given values and the ones found by setup()
+ //
+ void updateIni();
};
+// manages global and portable instances
+//
class InstanceManager
{
public:
+ // there is only one manager; this isn't called instance() because it's hella
+ // confusing
+ //
static InstanceManager& singleton();
+ // overrides instance name found in registry
+ //
void overrideInstance(const QString& instanceName);
+
+ // overrides profile name from INI for currentInstance()
+ //
void overrideProfile(const QString& profileName);
+ // returns a game plugin that considers the given directory valid
+ //
+ // this will check for an INI file in the directory and use its game name
+ // and directory if available
+ //
+ // if there is no INI, if it's missing these values or if there are no game
+ // plugins that can handle these values, this returns the first plugin that
+ // considers the given directory valid
+ //
+ // returns null if all of this fails
+ //
const MOBase::IPluginGame* gamePluginForDirectory(
- const QDir& dir, const PluginContainer& plugins) const;
+ const QString& dir, const PluginContainer& plugins) const;
+ // clears the instance name from the registry; on restart, this will make MO
+ // either select the portable instance if it exists, or display the instance
+ // selection/creation dialog
+ //
void clearCurrentInstance();
+
+ // returns the current instance from the registry; this may be empty if the
+ // instance name in the registry is empty or non-existent and there is no
+ // portable instance set up
+ //
std::optional<Instance> currentInstance() const;
+
+ // sets the instance name in the registry so the same instance is opened next
+ // time MO runs
+ //
void setCurrentInstance(const QString &name);
+ // whether MO should allow the user to change the current instance from the
+ // user interface
+ //
bool allowedToChangeInstance() const;
- static bool isPortablePath(const QString& dataPath);
- static QString portablePath();
+
+ // whether a portable instance exists; this basically checks for an INI in
+ // the application directory
+ //
bool portableInstanceExists() const;
- QString instancesPath() const;
- QStringList instanceNames() const;
- std::vector<QDir> instancePaths() const;
+ // whether any instance exists, whether global or portable
+ //
+ bool hasAnyInstances() const;
+
+ // returns the absolute path to the portable instance, regardless of whether
+ // one exists
+ //
+ QString portablePath() const;
+ // returns the absolute path to the directory that contains global instances
+ // (typically AppData/Local/ModOrganizer)
+ //
+ QString globalInstancesRootPath() const;
+
+ // returns the list of absolute path to all existing global instances; this
+ // does not include the portable instance
+ //
+ std::vector<QString> globalInstancePaths() const;
+
+ // returns `name` modified so that it is a valid instance name
+ //
QString sanitizeInstanceName(const QString &name) const;
+
+ // sanitizes the given instance name and either
+ // 1) returns it if there is no instance with this name
+ // 2) tries to add " (N)" at the end until it works
+ //
+ // may return an empty string if no unique name can be found
+ //
QString makeUniqueName(const QString& instanceName) const;
+
+ // returns whether a global instance with this name already exists
+ //
bool instanceExists(const QString& instanceName) const;
+
+ // returns whether the given instance name would be a valid name; this does
+ // not check whether the instance already exists, it's basiscally just a check
+ // against what sanitizeInstanceName() returns
+ //
bool validInstanceName(const QString& instanceName) const;
+
+ // returns the absolute path of a global instance with the given name; this
+ // does not check if the name is valid or if exists
+ //
QString instancePath(const QString& instanceName) const;
- static QString iniPath(const QDir& instanceDir);
+
+ // returns the absolute path to the INI file for the given instance directory;
+ // the file may not exist
+ //
+ QString iniPath(const QString& instanceDir) const;
private:
InstanceManager();
- bool portableInstallIsLocked() const;
private:
- bool m_overrideInstance{false};
- QString m_overrideInstanceName;
- bool m_overrideProfile{false};
- QString m_overrideProfileName;
+ std::optional<QString> m_overrideInstanceName;
+ std::optional<QString> m_overrideProfileName;
};
+
+// see setupInstance()
+//
+enum class SetupInstanceResults
+{
+ Okay,
+ TryAgain,
+ SelectAnother,
+ Exit
+};
+
+// if there are no instances configured, global or portable, shows the
+// create instance dialog and returns the new instance or empty if the user
+// cancelled
+//
+// if there is at least one instance available, unconditionally show the
+// instance manager dialog and returns the selected instance or empty if the
+// user cancelled
+//
+std::optional<Instance> selectInstance();
+
+// calls instance.setup() tries to handle problems by itself:
+//
+// - if the ini is missing some information, will show dialogs and ask the user
+// to fill in what's required (such as the game directory, variant, etc.);
+// if successful, returns TryAgain and setupInstance() can be called again
+// with the same instance
+//
+// - if the instance cannot be used (no game plugin found for it, ini can't
+// be read, etc.), returns SelectAnother
+//
+// - if the user cancels at any point, returns Exit
+//
+// - if the instance has been set up correctly, returns Okay
+//
+SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc);
+
#endif // MODORGANIZER_INSTANCEMANAGER_INCLUDED
diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp
index 3b6daeb8..b461d39d 100644
--- a/src/instancemanagerdialog.cpp
+++ b/src/instancemanagerdialog.cpp
@@ -13,292 +13,93 @@
using namespace MOBase;
-class InstanceInfo
+// returns the icon for the given instance or an empty 32x32 icon if the game
+// plugin couldn't be found
+//
+QIcon instanceIcon(const PluginContainer& pc, const Instance& i)
{
-public:
- struct Object
- {
- QString path;
- bool mandatoryDelete;
+ const auto* game = InstanceManager::singleton()
+ .gamePluginForDirectory(i.directory(), pc);
- Object(QString p, bool d=false)
- : path(std::move(p)), mandatoryDelete(d)
- {
- }
-
- bool operator<(const Object& o) const
- {
- if (mandatoryDelete && !o.mandatoryDelete) {
- return true;
- } else if (!mandatoryDelete && o.mandatoryDelete) {
- return false;
- }
-
- return false;
- }
- };
+ if (game)
+ return game->gameIcon();
+ QPixmap empty(32, 32);
+ empty.fill(QColor(0, 0, 0, 0));
+ return QIcon(empty);
+}
- InstanceInfo(QDir dir, bool isPortable)
- : m_portable(isPortable)
- {
- setDir(dir);
- }
-
- void setDir(const QDir& dir)
- {
- m_dir = dir;
- m_settings.reset(new Settings(InstanceManager::iniPath(dir)));
- }
-
- QString name() const
- {
- if (m_portable) {
- return QObject::tr("Portable");
- } else {
- return m_dir.dirName();
- }
- }
+// pops up a dialog to ask for an instance name when renaming
+//
+QString getInstanceName(
+ QWidget* parent, const QString& title, const QString& moreText,
+ const QString& label, const QString& oldName={})
+{
+ auto& m = InstanceManager::singleton();
- QString gameName() const
- {
- if (auto n=m_settings->game().name()) {
- if (auto e=m_settings->game().edition()) {
- if (!e->isEmpty()) {
- return *n + " (" + *e + ")";
- }
- }
+ QDialog dlg(parent);
+ dlg.setWindowTitle(title);
- return *n;
- } else {
- return {};
- }
- }
+ auto* ly = new QVBoxLayout(&dlg);
- QString gamePath() const
- {
- if (auto n=m_settings->game().directory()) {
- return QDir::toNativeSeparators(*n);
- } else {
- return {};
- }
- }
+ auto* bb = new QDialogButtonBox(
+ QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
- QString location() const
- {
- return QDir::toNativeSeparators(m_dir.path());
- }
+ auto* text = new QLineEdit(oldName);
+ text->selectAll();
- QString baseDirectory() const
- {
- return QDir::toNativeSeparators(m_settings->paths().base());
- }
+ auto* error = new QLabel;
- QString iniFile() const
- {
- return InstanceManager::iniPath(m_dir);
+ if (!moreText.isEmpty()) {
+ auto* lb = new QLabel(moreText);
+ lb->setWordWrap(true);
+ ly->addWidget(lb);
+ ly->addSpacing(10);
}
- QIcon icon(const PluginContainer& plugins) const
- {
- const auto* game = InstanceManager::singleton().gamePluginForDirectory(
- m_dir, plugins);
-
- if (game)
- return game->gameIcon();
+ auto* lb = new QLabel(label);
+ lb->setWordWrap(true);
+ ly->addWidget(lb);
- QPixmap empty(32, 32);
- empty.fill(QColor(0, 0, 0, 0));
- return QIcon(empty);
- }
+ ly->addWidget(text);
+ ly->addWidget(error);
+ ly->addStretch();
+ ly->addWidget(bb);
- bool isPortable() const
- {
- return m_portable;
- }
+ auto check = [&] {
+ bool okay = false;
- bool isActive() const
- {
- auto& m = InstanceManager::singleton();
+ if (text->text().isEmpty()) {
+ error->setText("");
+ } else if (!m.validInstanceName(text->text())) {
+ error->setText(QObject::tr("The instance name must be a valid folder name."));
+ } else {
+ const auto name = m.sanitizeInstanceName(text->text());
- if (auto i=m.currentInstance())
- {
- if (m_portable) {
- return i->isPortable();
+ if ((name != oldName) && m.instanceExists(text->text())) {
+ error->setText(QObject::tr("An instance with this name already exists."));
} else {
- return (i->name() == name());
- }
- }
-
- return false;
- }
-
- // returns a list of files and folders that must be deleted when deleting
- // this instance
- //
- std::vector<Object> objectsForDeletion() const
- {
- // native separators and ending slash
- auto prettyDir = [](auto s) {
- if (!s.endsWith("/") || !s.endsWith("\\")) {
- s += "/";
- }
-
- return QDir::toNativeSeparators(s);
- };
-
- // native separators
- auto prettyFile = [](auto s) {
- return QDir::toNativeSeparators(s);
- };
-
-
- // lowercase, native separators and ending slash
- auto canonicalDir = [](auto s) {
- s = s.toLower();
- if (!s.endsWith("/") || !s.endsWith("\\")) {
- s += "/";
- }
-
- return QDir::toNativeSeparators(s);
- };
-
- // lower and native separators
- auto canonicalFile = [](auto s) {
- return QDir::toNativeSeparators(s.toLower());
- };
-
-
-
- // whether the given directory is contained in the root
- auto dirInRoot = [&](auto root, auto dir) {
- return canonicalDir(dir).startsWith(canonicalDir(root));
- };
-
- // whether the given file is contained in the root
- auto fileInRoot = [&](auto root, auto file) {
- return canonicalFile(file).startsWith(canonicalDir(root));
- };
-
-
-
-
- const auto loc = location();
- const auto base = m_settings->paths().base();
-
-
- // directories that might contain the individual files and directories
- // set in the path settings
- std::vector<Object> roots;
-
- // a portable instance has its location in the installation directory,
- // don't delete that
- if (!isPortable()) {
- if (QDir(loc).exists()) {
- roots.push_back({loc, true});
- }
- }
-
- // the base directory is the location directory by default, don't add it
- // if it's the same
- if (canonicalDir(base) != canonicalDir(loc)) {
- if (QDir(base).exists()) {
- roots.push_back({base, false});
- }
- }
-
-
- // all the directories that are part of an instance; none of them are
- // mandatory for deletion
- const std::vector<Object> dirs = {
- m_settings->paths().downloads(),
- m_settings->paths().mods(),
- m_settings->paths().cache(),
- m_settings->paths().profiles(),
- m_settings->paths().overwrite(),
- m_dir.filePath(QString::fromStdWString(AppConfig::dumpsDir())),
- m_dir.filePath(QString::fromStdWString(AppConfig::logPath())),
- };
-
- // all the files that are part of an instance
- const std::vector<Object> files = {
- {iniFile(), true}, // the ini file must be deleted
- };
-
-
- // this will contain the root directories, plus all the individual
- // directories that are not inside these roots
- std::vector<Object> cleanDirs;
-
- for (const auto& f : dirs) {
- bool inRoots = false;
-
- for (const auto& root : roots) {
- if (dirInRoot(root.path, f.path)) {
- inRoots = true;
- break;
- }
- }
-
- if (!inRoots) {
- // not in roots, this is a path that was changed by the user; make
- // sure it exists
- if (QDir(f.path).exists()) {
- cleanDirs.push_back({prettyDir(f.path), f.mandatoryDelete});
- }
- }
- }
-
- // prepending the roots
- for (auto itor=roots.rbegin(); itor!=roots.rend(); ++itor) {
- cleanDirs.insert(
- cleanDirs.begin(),
- {prettyDir(itor->path), itor->mandatoryDelete});
- }
-
-
-
- // this will contain the individual files that are not inside the roots;
- // not that this only contains the INI file for now, so most of this is
- // useless
- std::vector<Object> cleanFiles;
-
- for (const auto& f : files) {
- bool inRoots = false;
-
- for (const auto& root : roots) {
- if (fileInRoot(root.path, f.path)) {
- inRoots = true;
- break;
- }
- }
-
- if (!inRoots) {
- // not in roots, this is a path that was changed by the user; make
- // sure it exists
- if (QFileInfo(f.path).exists()) {
- cleanFiles.push_back({prettyFile(f.path), f.mandatoryDelete});
- }
+ okay = true;
}
}
+ error->setVisible(!okay);
+ bb->button(QDialogButtonBox::Ok)->setEnabled(okay);
+ };
- // contains all the directories and files to be deleted
- std::vector<Object> all;
- all.insert(all.end(), cleanDirs.begin(), cleanDirs.end());
- all.insert(all.end(), cleanFiles.begin(), cleanFiles.end());
+ QObject::connect(text, &QLineEdit::textChanged, [&] { check(); });
+ QObject::connect(bb, &QDialogButtonBox::accepted, [&]{ dlg.accept(); });
+ QObject::connect(bb, &QDialogButtonBox::rejected, [&]{ dlg.reject(); });
- // mandatory on top
- std::stable_sort(all.begin(), all.end());
+ check();
- return all;
+ dlg.resize({400, 120});
+ if (dlg.exec() != QDialog::Accepted) {
+ return {};
}
-private:
- const bool m_portable;
- QDir m_dir;
- std::unique_ptr<Settings> m_settings;
-};
+ return m.sanitizeInstanceName(text->text());
+}
InstanceManagerDialog::~InstanceManagerDialog() = default;
@@ -310,7 +111,7 @@ InstanceManagerDialog::InstanceManagerDialog(
{
ui->setupUi(this);
- ui->splitter->setSizes({200, 1});
+ ui->splitter->setSizes({250, 1});
ui->splitter->setStretchFactor(0, 0);
ui->splitter->setStretchFactor(1, 1);
@@ -345,14 +146,40 @@ InstanceManagerDialog::InstanceManagerDialog(
connect(ui->close, &QPushButton::clicked, [&]{ close(); });
}
+void InstanceManagerDialog::showEvent(QShowEvent* e)
+{
+ // there might not be a global Settings object if this is called on startup
+ // when there's no current instance
+ const auto* s = Settings::maybeInstance();
+
+ if (s) {
+ s->geometry().restoreGeometry(this);
+ }
+
+ QDialog::showEvent(e);
+}
+
+void InstanceManagerDialog::done(int r)
+{
+ // there might not be a global Settings object if this is called on startup
+ // when there's no current instance
+ auto* s = Settings::maybeInstance();
+
+ if (s) {
+ s->geometry().saveGeometry(this);
+ }
+
+ QDialog::done(r);
+}
+
void InstanceManagerDialog::updateInstances()
{
auto& m = InstanceManager::singleton();
m_instances.clear();
- for (auto&& d : m.instancePaths()) {
- m_instances.push_back(std::make_unique<InstanceInfo>(d, false));
+ for (auto&& d : m.globalInstancePaths()) {
+ m_instances.push_back(std::make_unique<Instance>(d, false));
}
// sort first, prepend portable after so it's always on top
@@ -363,7 +190,12 @@ void InstanceManagerDialog::updateInstances()
if (m.portableInstanceExists()) {
m_instances.insert(
m_instances.begin(),
- std::make_unique<InstanceInfo>(m.portablePath(), true));
+ std::make_unique<Instance>(m.portablePath(), true));
+ }
+
+ // read all inis, ignore errors
+ for (auto&& i : m_instances) {
+ i->readFromIni();
}
}
@@ -374,14 +206,14 @@ void InstanceManagerDialog::updateList()
m_model->clear();
- const std::size_t NoSel = -1;
- std::size_t sel = NoSel;
+ std::size_t sel = NoSelection;
+ // creating items for instances
for (std::size_t i=0; i<m_instances.size(); ++i) {
const auto& ii = *m_instances[i];
auto* item = new QStandardItem(ii.name());
- item->setIcon(ii.icon(m_pc));
+ item->setIcon(instanceIcon(m_pc, ii));
m_model->appendRow(item);
@@ -396,7 +228,7 @@ void InstanceManagerDialog::updateList()
if (m_instances.empty()) {
select(-1);
} else {
- if (sel == NoSel) {
+ if (sel == NoSelection) {
if (prevSelIndex >= m_instances.size()) {
sel = m_instances.size() - 1;
} else {
@@ -462,10 +294,16 @@ void InstanceManagerDialog::openSelectedInstance()
return;
}
- if (m_instances[i]->isPortable()) {
+ const auto& to = *m_instances[i];
+
+ if (!confirmSwitch(to)) {
+ return;
+ }
+
+ if (to.isPortable()) {
InstanceManager::singleton().setCurrentInstance("");
} else {
- InstanceManager::singleton().setCurrentInstance(m_instances[i]->name());
+ InstanceManager::singleton().setCurrentInstance(to.name());
}
if (m_restartOnSelect) {
@@ -475,74 +313,36 @@ void InstanceManagerDialog::openSelectedInstance()
accept();
}
-QString getInstanceName(
- QWidget* parent, const QString& title, const QString& moreText,
- const QString& label, const QString& oldName={})
+bool InstanceManagerDialog::confirmSwitch(const Instance& to)
{
- auto& m = InstanceManager::singleton();
-
- QDialog dlg(parent);
- dlg.setWindowTitle(title);
-
- auto* ly = new QVBoxLayout(&dlg);
-
- auto* bb = new QDialogButtonBox(
- QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
-
- auto* text = new QLineEdit(oldName);
- text->selectAll();
-
- auto* error = new QLabel;
+ // there might not be a global Settings object if this is called on startup
+ // when there's no current instance
+ const auto* s = Settings::maybeInstance();
- if (!moreText.isEmpty()) {
- auto* lb = new QLabel(moreText);
- lb->setWordWrap(true);
- ly->addWidget(lb);
- ly->addSpacing(10);
+ // if there is are no settings, no instances are loaded and the confirmation
+ // wouldn't make sense
+ if (!s) {
+ return true;
}
- auto* lb = new QLabel(label);
- lb->setWordWrap(true);
- ly->addWidget(lb);
-
- ly->addWidget(text);
- ly->addWidget(error);
- ly->addStretch();
- ly->addWidget(bb);
-
- auto check = [&] {
- bool okay = false;
-
- if (text->text().isEmpty()) {
- error->setText("");
- } else if (!m.validInstanceName(text->text())) {
- error->setText(QObject::tr("The instance name must be a valid folder name."));
- } else {
- const auto name = m.sanitizeInstanceName(text->text());
-
- if ((name != oldName) && m.instanceExists(text->text())) {
- error->setText(QObject::tr("An instance with this name already exists."));
- } else {
- okay = true;
- }
- }
-
- error->setVisible(!okay);
- bb->button(QDialogButtonBox::Ok)->setEnabled(okay);
- };
-
- QObject::connect(text, &QLineEdit::textChanged, [&] { check(); });
- QObject::connect(bb, &QDialogButtonBox::accepted, [&]{ dlg.accept(); });
- QObject::connect(bb, &QDialogButtonBox::rejected, [&]{ dlg.reject(); });
+ if (!s->interface().showChangeGameConfirmation()) {
+ // user disabled confirmation
+ return true;
+ }
- check();
+ MOBase::TaskDialog dlg(this);
- dlg.resize({400, 120});
- if (dlg.exec() != QDialog::Accepted) {
- return {};
- }
+ const auto r = dlg
+ .title(tr("Switching instances"))
+ .main(tr("Mod Organizer must restart to manage the instance '%1'.")
+ .arg(to.name()))
+ .content(tr("This confirmation can be disabled in the settings."))
+ .icon(QMessageBox::Question)
+ .button({tr("Restart Mod Organizer"), QMessageBox::Ok})
+ .button({tr("Cancel"), QMessageBox::Cancel})
+ .exec();
- return m.sanitizeInstanceName(text->text());
+ return (r == QMessageBox::Ok);
}
void InstanceManagerDialog::rename()
@@ -561,6 +361,8 @@ void InstanceManagerDialog::rename()
return;
}
+
+ // getting new name
const auto newName = getInstanceName(
this, tr("Rename instance"), "", tr("Instance name"), i->name());
@@ -568,11 +370,16 @@ void InstanceManagerDialog::rename()
return;
}
- const QString src = i->location();
+
+ // renaming
+ const QString src = i->directory();
const QString dest = QDir::toNativeSeparators(
- QFileInfo(i->location()).dir().path() + "/" + newName);
+ QFileInfo(src).dir().path() + "/" + newName);
+
+ log::info("renaming {} to {}", src, dest);
const auto r = shell::Rename(src, dest, false);
+
if (!r) {
QMessageBox::critical(
this, tr("Error"),
@@ -582,15 +389,21 @@ void InstanceManagerDialog::rename()
return;
}
+
+ // updating ui
+ auto newInstance = std::make_unique<Instance>(dest, false);
+ i = newInstance.get();
+
m_model->item(selIndex)->setText(newName);
- i->setDir(dest);
+ m_instances[selIndex] = std::move(newInstance);
+
fillData(*i);
}
void InstanceManagerDialog::exploreLocation()
{
if (const auto* i=singleSelection()) {
- shell::Explore(i->location());
+ shell::Explore(i->directory());
}
}
@@ -604,14 +417,14 @@ void InstanceManagerDialog::exploreBaseDirectory()
void InstanceManagerDialog::exploreGame()
{
if (const auto* i=singleSelection()) {
- shell::Explore(i->gamePath());
+ shell::Explore(i->gameDirectory());
}
}
void InstanceManagerDialog::openINI()
{
if (const auto* i=singleSelection()) {
- shell::Open(i->iniFile());
+ shell::Open(i->iniPath());
}
}
@@ -629,6 +442,8 @@ void InstanceManagerDialog::deleteInstance()
return;
}
+ // creating dialog
+
const auto Recycle = QMessageBox::Save;
const auto Delete = QMessageBox::Yes;
const auto Cancel = QMessageBox::Cancel;
@@ -652,6 +467,8 @@ void InstanceManagerDialog::deleteInstance()
list->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
list->setMaximumHeight(160);
+
+ // filling the list
for (const auto& f : files) {
auto* item = new QListWidgetItem(f.path);
@@ -676,6 +493,7 @@ void InstanceManagerDialog::deleteInstance()
}
+ // gathering all the selected items
QStringList selected;
for (int i=0; i<list->count(); ++i) {
@@ -691,10 +509,14 @@ void InstanceManagerDialog::deleteInstance()
return;
}
+
+ // deleting
if (!doDelete(selected, (r == Recycle))) {
return;
}
+
+ // updating ui
updateInstances();
updateList();
}
@@ -707,6 +529,15 @@ void InstanceManagerDialog::setRestartOnSelect(bool b)
bool InstanceManagerDialog::doDelete(const QStringList& files, bool recycle)
{
+ // logging
+ for (auto&& f : files) {
+ if (recycle) {
+ log::info("will recycle {}", f);
+ } else {
+ log::info("will delete {}", f);
+ }
+ }
+
if (MOBase::shellDelete(files, recycle, this)) {
return true;
}
@@ -756,7 +587,7 @@ void InstanceManagerDialog::createNew()
updateInstances();
updateList();
- select(dlg.instanceName());
+ select(dlg.creationInfo().instanceName);
}
std::size_t InstanceManagerDialog::singleSelectionIndex() const
@@ -771,17 +602,7 @@ std::size_t InstanceManagerDialog::singleSelectionIndex() const
return static_cast<std::size_t>(sel.indexes()[0].row());
}
-InstanceInfo* InstanceManagerDialog::singleSelection()
-{
- const auto i = singleSelectionIndex();
- if (i == NoSelection) {
- return nullptr;
- }
-
- return m_instances[i].get();
-}
-
-const InstanceInfo* InstanceManagerDialog::singleSelection() const
+const Instance* InstanceManagerDialog::singleSelection() const
{
const auto i = singleSelectionIndex();
if (i == NoSelection) {
@@ -791,13 +612,13 @@ const InstanceInfo* InstanceManagerDialog::singleSelection() const
return m_instances[i].get();
}
-void InstanceManagerDialog::fillData(const InstanceInfo& ii)
+void InstanceManagerDialog::fillData(const Instance& ii)
{
ui->name->setText(ii.name());
- ui->location->setText(ii.location());
+ ui->location->setText(ii.directory());
ui->baseDirectory->setText(ii.baseDirectory());
ui->gameName->setText(ii.gameName());
- ui->gameDir->setText(ii.gamePath());
+ ui->gameDir->setText(ii.gameDirectory());
setButtonsEnabled(true);
const auto& m = InstanceManager::singleton();
diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h
index 5b08ffc2..86fe0dac 100644
--- a/src/instancemanagerdialog.h
+++ b/src/instancemanagerdialog.h
@@ -6,9 +6,11 @@
namespace Ui { class InstanceManagerDialog; };
-class InstanceInfo;
+class Instance;
class PluginContainer;
+// a dialog to manage existing instances
+//
class InstanceManagerDialog : public QDialog
{
Q_OBJECT
@@ -19,49 +21,126 @@ public:
~InstanceManagerDialog();
+ // selects the instance having the given index in the list
+ //
void select(std::size_t i);
+
+ // selects the instance by name
+ //
void select(const QString& name);
+
+ // select the instance that is currently in use in MO
+ //
void selectActiveInstance();
+
+ // switches to the selected instance; restarts MO, unless
+ // was called setRestartOnSelect(false)
+ //
void openSelectedInstance();
+ // renames the currently selected instance
+ //
void rename();
+
+ // explores the directory of the selected instance
+ //
void exploreLocation();
+
+ // explores the base directory of the selected instance
+ //
void exploreBaseDirectory();
+
+ // explores the game directory of the selected instance
+ //
void exploreGame();
+
+ // converts the selected, portable instance to a global one; not implemented
+ //
void convertToGlobal();
+
+ // converts the selected, global instance to a portable one; not implemented
+ //
void convertToPortable();
+
+ // opens the ini of the selected instance in the shell
+ //
void openINI();
+
+ // deletes the selected instance
+ //
void deleteInstance();
+
+ // sets whether the dialog should restart MO when selecting an instance; this
+ // is false on startup when no instances exist
+ //
void setRestartOnSelect(bool b);
+
+ // saves geometry
+ //
+ void done(int r) override;
+
+protected:
+ // restores geometry
+ //
+ void showEvent(QShowEvent* e) override;
+
private:
static const std::size_t NoSelection = -1;
std::unique_ptr<Ui::InstanceManagerDialog> ui;
const PluginContainer& m_pc;
- std::vector<std::unique_ptr<InstanceInfo>> m_instances;
+ std::vector<std::unique_ptr<Instance>> m_instances;
MOBase::FilterWidget m_filter;
QStandardItemModel* m_model;
bool m_restartOnSelect;
+ // refreshes the list instances from disk
+ //
void updateInstances();
+ // updates the ui for the selected instance
+ //
void onSelection();
+
+ // opens the create instance dialog
+ //
void createNew();
+ // shows a confirmation to the user before switching
+ //
+ bool confirmSwitch(const Instance& to);
+
+
+ // returns the index of selected instance, NoSelection if none
+ //
std::size_t singleSelectionIndex() const;
- InstanceInfo* singleSelection();
- const InstanceInfo* singleSelection() const;
+ // returns the InstanceInfo associated with the selected instance, null if
+ // none
+ //
+ const Instance* singleSelection() const;
+
+ // fills the instance list on the ui
+ //
void updateList();
- void fillData(const InstanceInfo& ii);
+
+ // fills the ui for the selected instance
+ //
+ void fillData(const Instance& ii);
+
+ // clears the ui when there's no selection
+ //
void clearData();
+
+ // enables/disables buttons like rename, explore...
+ //
void setButtonsEnabled(bool b);
- bool deletePortable(const InstanceInfo& ii);
- bool deleteGlobal(const InstanceInfo& ii);
+ // deletes the given files, returns false on error
+ //
bool doDelete(const QStringList& files, bool recycle);
};
diff --git a/src/instancemanagerdialog.ui b/src/instancemanagerdialog.ui
index 410b58c1..44f4ff1f 100644
--- a/src/instancemanagerdialog.ui
+++ b/src/instancemanagerdialog.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>689</width>
- <height>413</height>
+ <width>884</width>
+ <height>539</height>
</rect>
</property>
<property name="windowTitle">
@@ -56,7 +56,7 @@
<item>
<widget class="LinkLabel" name="whatis">
<property name="text">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer/wiki/Instance&quot;&gt;What is an instance?&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer/wiki/Instances&quot;&gt;What is an instance?&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
@@ -330,7 +330,7 @@
<item>
<widget class="QPushButton" name="deleteInstance">
<property name="text">
- <string>Delete instance</string>
+ <string>Delete instance...</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
diff --git a/src/loglist.cpp b/src/loglist.cpp
index fad4678c..8df21111 100644
--- a/src/loglist.cpp
+++ b/src/loglist.cpp
@@ -345,7 +345,10 @@ bool createAndMakeWritable(const std::wstring &subPath) {
bool setLogDirectory(const QString& dir)
{
- const auto logFile = dir + "/logs/mo_interface.log";
+ const auto logFile =
+ dir + "/" +
+ QString::fromStdWString(AppConfig::logPath()) + "/" +
+ QString::fromStdWString(AppConfig::logFileName());
if (!createAndMakeWritable(AppConfig::logPath())) {
return false;
diff --git a/src/main.cpp b/src/main.cpp
index e56197c9..554ed006 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,625 +1,53 @@
-/*
-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 "mainwindow.h"
-#include "singleinstance.h"
+#include "multiprocess.h"
#include "loglist.h"
-#include "selectiondialog.h"
#include "moapplication.h"
-#include "tutorialmanager.h"
-#include "nxmaccessmanager.h"
-#include "instancemanager.h"
-#include "instancemanagerdialog.h"
-#include "createinstancedialog.h"
-#include "createinstancedialogpages.h"
#include "organizercore.h"
-#include "env.h"
-#include "envmodule.h"
#include "commandline.h"
+#include "env.h"
+#include "thread_utils.h"
#include "shared/util.h"
-#include "shared/appconfig.h"
-
-#include <imoinfo.h>
-#include <report.h>
-#include <usvfs.h>
#include <log.h>
-#include <utility.h>
-
-// 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);
-
-void 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);
-}
thread_local LPTOP_LEVEL_EXCEPTION_FILTER g_prevExceptionFilter = nullptr;
thread_local std::terminate_handler g_prevTerminateHandler = nullptr;
-LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs)
-{
- const std::wstring& dumpPath = OrganizerCore::crashDumpsPath();
-
- 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 {}).",
- r, GetLastError());
- }
-
- if (g_prevExceptionFilter && ptrs)
- return g_prevExceptionFilter(ptrs);
- else
- return EXCEPTION_CONTINUE_SEARCH;
-}
-
-void onTerminate() noexcept
-{
- __try
- {
- // force an exception to get a valid stack trace for this thread
- *(int*)0 = 42;
- }
- __except
- (
- onUnhandledException(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER
- )
- {
- }
-
- if (g_prevTerminateHandler) {
- g_prevTerminateHandler();
- } else {
- std::abort();
- }
-}
-
-void setExceptionHandlers()
-{
- g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException);
- g_prevTerminateHandler = std::set_terminate(onTerminate);
-}
-
-// 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);
-}
-
-QString getSplashPath(
- const Settings& settings, const QString& dataPath,
- const MOBase::IPluginGame* game)
-{
- 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;
-}
-
-std::unique_ptr<QSplashScreen> createSplash(
- 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 {};
- }
-
- auto splash = std::make_unique<QSplashScreen>(image);
- settings.geometry().centerOnMainWindowMonitor(splash.get());
-
- splash->show();
- splash->activateWindow();
-
- return splash;
-}
-
-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();
-
- 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);
-
- 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 {};
-}
-
-std::optional<Instance> selectInstance()
-{
- auto& m = InstanceManager::singleton();
-
- NexusInterface ni(nullptr);
- PluginContainer pc(nullptr);
- pc.loadPlugins();
-
- if (m.instancePaths().empty() && !m.portableInstanceExists()) {
- // no instances configured
- CreateInstanceDialog dlg(pc, nullptr);
- if (dlg.exec() != QDialog::Accepted) {
- return {};
- }
-
- return m.currentInstance();
- }
-
-
- InstanceManagerDialog dlg(pc);
- dlg.setRestartOnSelect(false);
-
- dlg.show();
- dlg.activateWindow();
- dlg.raise();
-
- if (dlg.exec() != QDialog::Accepted) {
- return {};
- }
-
- return m.currentInstance();
-}
-
-enum class SetupInstanceResults
-{
- Ok,
- TryAgain,
- SelectAnother,
- Exit
-};
-
+int forwardToPrimary(MOMultiProcess& multiProcess, const cl::CommandLine& cl);
-void criticalOnTop(const QString& message)
-{
- QMessageBox mb(QMessageBox::Critical, QObject::tr("Mod Organizer"), message);
-
- mb.show();
- mb.activateWindow();
- mb.raise();
- mb.exec();
-}
-
-
-SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc)
+int main(int argc, char *argv[])
{
- const auto setupResult = instance.setup(pc);
-
- switch (setupResult)
- {
- case Instance::SetupResults::Ok:
- {
- return SetupInstanceResults::Ok;
- }
-
- case Instance::SetupResults::BadIni:
- {
- criticalOnTop(
- QObject::tr("Cannot open instance '%1', failed to read INI file %2.")
- .arg(instance.name()).arg(instance.iniPath()));
-
- return SetupInstanceResults::SelectAnother;
- }
-
- 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()));
-
- CreateInstanceDialog dlg(pc, nullptr);
- dlg.setSinglePage<cid::GamePage>(instance.name());
-
- dlg.show();
- dlg.activateWindow();
- dlg.raise();
-
- if (dlg.exec() != QDialog::Accepted) {
- return SetupInstanceResults::Exit;
- }
-
- instance.setGame(
- dlg.creationInfo().game->gameName(),
- dlg.creationInfo().gameLocation);
+ TimeThis tt("main()");
- return SetupInstanceResults::TryAgain;
- }
-
- 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()));
-
- return SetupInstanceResults::SelectAnother;
- }
-
- 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()));
-
- CreateInstanceDialog dlg(pc, nullptr);
- dlg.setSinglePage<cid::GamePage>(instance.name());
-
- dlg.show();
- dlg.activateWindow();
- dlg.raise();
-
- if (dlg.exec() != QDialog::Accepted) {
- return SetupInstanceResults::Exit;
- }
-
- instance.setGame(
- dlg.creationInfo().game->gameName(),
- dlg.creationInfo().gameLocation);
-
- return SetupInstanceResults::TryAgain;
- }
-
- case Instance::SetupResults::MissingVariant:
- {
- CreateInstanceDialog dlg(pc, nullptr);
-
- dlg.getPage<cid::GamePage>()->select(
- instance.gamePlugin(), instance.gameDirectory());
-
- dlg.setSinglePage<cid::VariantsPage>(instance.name());
-
- dlg.show();
- dlg.activateWindow();
- dlg.raise();
-
- if (dlg.exec() != QDialog::Accepted) {
- return SetupInstanceResults::Exit;
- }
-
- instance.setVariant(dlg.creationInfo().gameVariant);
-
- return SetupInstanceResults::TryAgain;
- }
+ MOShared::SetThisThreadName("main");
+ setExceptionHandlers();
- default:
- {
- return SetupInstanceResults::Exit;
- }
+ cl::CommandLine cl;
+ if (auto r=cl.run(GetCommandLineW())) {
+ return *r;
}
-}
-
-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: {}",
- createVersionInfo().displayString(3), GITID,
- QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString());
- log::info("data path: {}", dataPath);
-
- if (InstanceManager::isPortablePath(dataPath)) {
- log::debug("this is a portable instance");
- }
+ initLogging();
- log::info("working directory: {}", QDir::currentPath());
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ MOApplication app(cl, argc, argv);
- if (!instance.secondary()) {
- purgeOldFiles();
+ MOMultiProcess multiProcess(cl.multiple());
+ if (multiProcess.ephemeral()) {
+ return forwardToPrimary(multiProcess, cl);
}
- QWindowsWindowFunctions::setWindowActivationBehavior(
- QWindowsWindowFunctions::AlwaysActivateWindow);
-
- try
- {
- Settings settings(
- dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()),
- true);
-
- 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());
-
- env::Environment env;
- env.dump(settings);
- settings.dump();
- sanityChecks(env);
-
- const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) {
- log::debug("loaded module {}", m.toString());
- checkIncompatibleModule(m);
- });
-
- // 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::singleton().clearCurrentInstance();
- return 1;
- }
-
- log::debug("initializing plugins");
- pluginContainer = std::make_unique<PluginContainer>(&organizer);
- pluginContainer->loadPlugins();
-
- for (;;)
- {
- const auto setupResult = setupInstance(currentInstance, *pluginContainer);
-
- if (setupResult == SetupInstanceResults::Ok) {
- break;
- } else if (setupResult == SetupInstanceResults::TryAgain) {
- continue;
- } else if (setupResult == SetupInstanceResults::SelectAnother) {
- InstanceManager::singleton().clearCurrentInstance();
- return RestartExitCode;
- } else {
- return 1;
- }
- }
-
- checkPathsForSanity(*currentInstance.gamePlugin(), settings);
-
- organizer.setManagedGame(currentInstance.gamePlugin());
- organizer.createDefaultProfile();
-
- log::info(
- "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();
-
- organizer.setCurrentProfile(currentInstance.profileName());
-
- if (auto r=handleCommandLine(cl, organizer)) {
- return *r;
- }
-
- auto splash = createSplash(settings, dataPath, currentInstance.gamePlugin());
-
- QString apiKey;
- if (GlobalSettings::nexusApiKey(apiKey)) {
- ni.getAccessManager()->apiCheck(apiKey);
- }
-
- log::debug("initializing tutorials");
- TutorialManager::init(
- qApp->applicationDirPath() + "/"
- + QString::fromStdWString(AppConfig::tutorialsPath()) + "/",
- &organizer);
-
- if (!application.setStyleFile(settings.interface().styleName().value_or(""))) {
- // disable invalid stylesheet
- settings.interface().setStyleName("");
- }
-
- int res = 1;
-
- { // scope to control lifetime of mainwindow
- // set up main window and its data structures
- MainWindow mainWindow(settings, organizer, *pluginContainer);
-
- ni.getAccessManager()->setTopLevelWidget(&mainWindow);
-
- QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application,
- SLOT(setStyleFile(QString)));
- QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer,
- SLOT(externalMessage(QString)));
-
- log::debug("displaying main window");
- mainWindow.show();
- mainWindow.activateWindow();
-
- if (splash) {
- // don't pass mainwindow as it just waits half a second for it
- // instead of proceding
- splash->finish(nullptr);
- }
-
- tt.stop();
-
- res = application.exec();
- mainWindow.close();
-
- ni.getAccessManager()->setTopLevelWidget(nullptr);
- }
-
- settings.geometry().resetIfNeeded();
- return res;
- }
- catch (const std::exception &e)
- {
- reportError(e.what());
- }
+ tt.stop();
- return 1;
+ return app.run(multiProcess);
}
-int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl)
+int forwardToPrimary(MOMultiProcess& multiProcess, const cl::CommandLine& cl)
{
if (cl.shortcut().isValid()) {
- instance.sendMessage(cl.shortcut().toString());
+ multiProcess.sendMessage(cl.shortcut().toString());
} else if (cl.nxmLink()) {
- instance.sendMessage(*cl.nxmLink());
+ multiProcess.sendMessage(*cl.nxmLink());
} else {
QMessageBox::information(
nullptr, QObject::tr("Mod Organizer"),
@@ -629,118 +57,55 @@ int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl)
return 0;
}
-void resetForRestart(cl::CommandLine& cl)
+LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs)
{
- LogModel::instance().clear();
- ResetExitFlag();
+ const auto path = OrganizerCore::getGlobalCoreDumpPath();
+ const auto type = OrganizerCore::getGlobalCoreDumpType();
- // make sure the log file isn't locked in case MO was restarted and
- // the previous instance gets deleted
- log::getDefault().setFile({});
+ const auto r = env::coredump(path.empty() ? nullptr : path.c_str(), type);
- // don't reprocess command line
- cl.clear();
+ if (r) {
+ log::error("ModOrganizer has crashed, core dump created.");
+ } else {
+ log::error("ModOrganizer has crashed, core dump failed");
+ }
+
+ // g_prevExceptionFilter somehow sometimes point to this function, making this
+ // recurse and create hundreds of core dump, not sure why
+ if (g_prevExceptionFilter && ptrs && g_prevExceptionFilter != onUnhandledException)
+ return g_prevExceptionFilter(ptrs);
+ else
+ return EXCEPTION_CONTINUE_SEARCH;
}
-int doOneRun(
- cl::CommandLine& cl, MOApplication& application, SingleInstance& instance)
+void onTerminate() noexcept
{
- TimeThis tt("doOneRun() to runApplication()");
-
- // resets things when MO is "restarted"
- resetForRestart(cl);
-
- auto& m = InstanceManager::singleton();
- auto currentInstance = m.currentInstance();
-
- if (!currentInstance)
+ __try
{
- currentInstance = selectInstance();
- if (!currentInstance) {
- return 1;
- }
+ // force an exception to get a valid stack trace for this thread
+ *(int*)0 = 42;
}
- else
+ __except
+ (
+ onUnhandledException(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER
+ )
{
- if (!currentInstance->directory().exists()) {
- // the previously used instance doesn't exist anymore
-
- 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()));
- }
-
- currentInstance = selectInstance();
- if (!currentInstance) {
- return 1;
- }
- }
}
- const QString dataPath = currentInstance->directory().path();
- application.setProperty("dataPath", dataPath);
-
- setExceptionHandlers();
-
- if (!setLogDirectory(dataPath)) {
- reportError("Failed to create log folder");
- InstanceManager::singleton().clearCurrentInstance();
- return 1;
+ if (g_prevTerminateHandler) {
+ g_prevTerminateHandler();
+ } else {
+ std::abort();
}
-
- log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
-
- tt.stop();
-
- return runApplication(application, cl, instance, dataPath, *currentInstance);
}
-int main(int argc, char *argv[])
+void setExceptionHandlers()
{
- cl::CommandLine cl;
-
- if (auto r=cl.run(GetCommandLineW())) {
- return *r;
- }
-
- TimeThis tt("main() to doOneRun()");
-
- SetThisThreadName("main");
-
- initLogging();
- auto application = MOApplication::create(argc, argv);
- addDllsToPath();
-
- SingleInstance instance(cl.multiple());
- if (instance.ephemeral()) {
- return forwardToPrimary(instance, cl);
+ if (g_prevExceptionFilter) {
+ // already called
+ return;
}
- tt.stop();
-
- if (cl.instance())
- InstanceManager::singleton().overrideInstance(*cl.instance());
-
- if (cl.profile()) {
- InstanceManager::singleton().overrideProfile(*cl.profile());
- }
-
- // makes plugin data path available to plugins, see
- // IOrganizer::getPluginDataPath()
- MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath());
-
- for (;;)
- {
- const auto r = doOneRun(cl, application, instance);
- if (r == RestartExitCode) {
- continue;
- }
-
- return r;
- }
+ g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException);
+ g_prevTerminateHandler = std::set_terminate(onTerminate);
}
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 2548264e..94bc5800 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -5087,7 +5087,7 @@ void MainWindow::on_actionSettings_triggered()
bool proxy = settings.network().useProxy();
DownloadManager *dlManager = m_OrganizerCore.downloadManager();
const bool oldCheckForUpdates = settings.checkForUpdates();
- const int oldMaxDumps = settings.diagnostics().crashDumpsMax();
+ const int oldMaxDumps = settings.diagnostics().maxCoreDumps();
SettingsDialog dialog(&m_PluginContainer, settings, this);
@@ -5174,7 +5174,7 @@ void MainWindow::on_actionSettings_triggered()
m_OrganizerCore.setLogLevel(settings.diagnostics().logLevel());
- if (settings.diagnostics().crashDumpsMax() != oldMaxDumps) {
+ if (settings.diagnostics().maxCoreDumps() != oldMaxDumps) {
m_OrganizerCore.cycleDiagnostics();
}
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index 466bd7bd..ace0dfeb 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -1768,16 +1768,16 @@ p, li { white-space: pre-wrap; }
<normaloff>:/MO/gui/instance_switch</normaloff>:/MO/gui/instance_switch</iconset>
</property>
<property name="text">
- <string>&amp;Change Game...</string>
+ <string>Manage Instan&amp;ces...</string>
</property>
<property name="iconText">
- <string>&amp;Change Game</string>
+ <string>Manage Instan&amp;ces...</string>
</property>
<property name="toolTip">
- <string>Open the Instance selection dialog to manage a different Game</string>
+ <string>Open the instance manager window to manage a different instance</string>
</property>
<property name="statusTip">
- <string>Open the Instance selection dialog to manage a different Game</string>
+ <string>Open the instance manager window to manage a different instance</string>
</property>
</action>
<action name="actionExit">
@@ -1878,6 +1878,11 @@ p, li { white-space: pre-wrap; }
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
+ <class>LinkLabel</class>
+ <extends>QLabel</extends>
+ <header location="global">linklabel.h</header>
+ </customwidget>
+ <customwidget>
<class>MOBase::LineEditClear</class>
<extends>QLineEdit</extends>
<header>lineeditclear.h</header>
@@ -1917,11 +1922,6 @@ p, li { white-space: pre-wrap; }
<extends>QStatusBar</extends>
<header>statusbar.h</header>
</customwidget>
- <customwidget>
- <class>LinkLabel</class>
- <extends>QLabel</extends>
- <header location="global">linklabel.h</header>
- </customwidget>
</customwidgets>
<resources>
<include location="resources.qrc"/>
diff --git a/src/moapplication.cpp b/src/moapplication.cpp
index 290666af..9139acbb 100644
--- a/src/moapplication.cpp
+++ b/src/moapplication.cpp
@@ -18,6 +18,22 @@ 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 "shared/error_report.h"
+#include "shared/util.h"
+#include <iplugingame.h>
#include <report.h>
#include <utility.h>
#include <log.h>
@@ -30,9 +46,15 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#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;
class ProxyStyle : public QProxyStyle {
public:
@@ -71,9 +93,50 @@ public:
};
-MOApplication::MOApplication(int& argc, char** argv)
- : QApplication(argc, argv)
+// 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(cl::CommandLine& cl, int& argc, char** argv)
+ : QApplication(argc, argv), m_cl(cl)
{
+ TimeThis tt("MOApplication()");
+
connect(&m_StyleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file){
log::debug("style file '{}' changed, reloading", file);
updateStyle(file);
@@ -81,12 +144,328 @@ MOApplication::MOApplication(int& argc, char** argv)
m_DefaultStyle = style()->objectName();
setStyle(new ProxyStyle(style()));
+ addDllsToPath();
}
-MOApplication MOApplication::create(int& argc, char** argv)
+int MOApplication::run(MOMultiProcess& multiProcess)
{
- QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
- return MOApplication(argc, argv);
+ TimeThis tt("MOApplication run() to doOneRun()");
+
+ auto& m = InstanceManager::singleton();
+
+ if (m_cl.instance())
+ m.overrideInstance(*m_cl.instance());
+
+ if (m_cl.profile()) {
+ m.overrideProfile(*m_cl.profile());
+ }
+
+ // makes plugin data path available to plugins, see
+ // IOrganizer::getPluginDataPath()
+ MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath());
+
+ // MO runs in a loop because it can be restarted in several ways, such as
+ // when switching instances or changing some settings
+ for (;;)
+ {
+ try
+ {
+ // resets things when MO is "restarted"
+ resetForRestart();
+
+ tt.stop();
+ const auto r = doOneRun(multiProcess);
+ if (r == RestartExitCode) {
+ continue;
+ }
+
+ return r;
+ }
+ catch (const std::exception &e)
+ {
+ reportError(e.what());
+ return 1;
+ }
+ }
+}
+
+int MOApplication::doOneRun(MOMultiProcess& multiProcess)
+{
+ TimeThis tt("MOApplication::doOneRun() instances");
+
+ // figuring out the current instance
+ auto currentInstance = getCurrentInstance();
+ if (!currentInstance) {
+ 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 = currentInstance->directory();
+ setProperty("dataPath", dataPath);
+
+ if (!setLogDirectory(dataPath)) {
+ reportError("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: {}", currentInstance->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
+ Settings settings(currentInstance->iniPath(), true);
+ log::getDefault().setLevel(settings.diagnostics().logLevel());
+ log::debug("using ini at '{}'", settings.filename());
+
+ OrganizerCore::setGlobalCoreDumpType(settings.diagnostics().coreDumpType());
+
+
+ tt.start("MOApplication::doOneRun() log and checks");
+
+ // logging and checking
+ env::Environment env;
+ env.dump(settings);
+ settings.dump();
+ sanity::checkEnvironment(env);
+
+ const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) {
+ log::debug("loaded module {}", m.toString());
+ sanity::checkIncompatibleModule(m);
+ });
+
+
+ // this must outlive `organizer`
+ std::unique_ptr<PluginContainer> pluginContainer;
+
+ // nexus interface
+ tt.start("MOApplication::doOneRun() NexusInterface");
+ log::debug("initializing nexus interface");
+ NexusInterface ni(&settings);
+
+ // organizer core
+ tt.start("MOApplication::doOneRun() OrganizerCore");
+ log::debug("initializing core");
+
+ OrganizerCore organizer(settings);
+ if (!organizer.bootstrap()) {
+ reportError("failed to set up data paths");
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
+
+ // plugins
+ tt.start("MOApplication::doOneRun() plugins");
+ log::debug("initializing plugins");
+
+ pluginContainer = std::make_unique<PluginContainer>(&organizer);
+ pluginContainer->loadPlugins();
+
+ // instance
+ if (auto r=setupInstanceLoop(*currentInstance, *pluginContainer)) {
+ return *r;
+ }
+
+ if (currentInstance->isPortable()) {
+ log::debug("this is a portable instance");
+ }
+
+ tt.start("MOApplication::doOneRun() OrganizerCore setup");
+
+ sanity::checkPaths(*currentInstance->gamePlugin(), settings);
+
+ // setting up organizer core
+ organizer.setManagedGame(currentInstance->gamePlugin());
+ organizer.createDefaultProfile();
+
+ log::info(
+ "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();
+ organizer.setCurrentProfile(currentInstance->profileName());
+
+ // checking command line
+ tt.start("MOApplication::doOneRun() command line");
+ if (auto r=m_cl.setupCore(organizer)) {
+ return *r;
+ }
+
+ // show splash
+ tt.start("MOApplication::doOneRun() splash");
+
+ MOSplash splash(
+ settings, currentInstance->directory(), currentInstance->gamePlugin());
+
+ tt.start("MOApplication::doOneRun() finishing");
+
+ // start an api check
+ QString apiKey;
+ if (GlobalSettings::nexusApiKey(apiKey)) {
+ ni.getAccessManager()->apiCheck(apiKey);
+ }
+
+ // tutorials
+ log::debug("initializing tutorials");
+ TutorialManager::init(
+ qApp->applicationDirPath() + "/"
+ + QString::fromStdWString(AppConfig::tutorialsPath()) + "/",
+ &organizer);
+
+ // styling
+ if (!setStyleFile(settings.interface().styleName().value_or(""))) {
+ // disable invalid stylesheet
+ settings.interface().setStyleName("");
+ }
+
+
+ int res = 1;
+
+ {
+ tt.start("MOApplication::doOneRun() MainWindow setup");
+ MainWindow mainWindow(settings, organizer, *pluginContainer);
+
+ // qt resets the thread name somewhere when creating the main window
+ MOShared::SetThisThreadName("main");
+
+ // the nexus interface can show dialogs, make sure they're parented to the
+ // main window
+ ni.getAccessManager()->setTopLevelWidget(&mainWindow);
+
+ QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), this,
+ SLOT(setStyleFile(QString)));
+
+ QObject::connect(&multiProcess, SIGNAL(messageSent(QString)), &organizer,
+ SLOT(externalMessage(QString)));
+
+
+ log::debug("displaying main window");
+ mainWindow.show();
+ mainWindow.activateWindow();
+ splash.close();
+
+ tt.stop();
+
+ res = exec();
+ mainWindow.close();
+
+ // main window is about to be destroyed
+ ni.getAccessManager()->setTopLevelWidget(nullptr);
+ }
+
+ // reset geometry if the flag was set from the settings dialog
+ settings.geometry().resetIfNeeded();
+
+ return res;
+}
+
+std::optional<Instance> MOApplication::getCurrentInstance()
+{
+ auto& m = InstanceManager::singleton();
+ auto currentInstance = m.currentInstance();
+
+ if (!currentInstance)
+ {
+ currentInstance = selectInstance();
+ }
+ else
+ {
+ if (!QDir(currentInstance->directory()).exists()) {
+ // the previously used instance doesn't exist anymore
+
+ if (m.hasAnyInstances()) {
+ MOShared::criticalOnTop(QObject::tr(
+ "Instance at '%1' not found. Select another instance.")
+ .arg(currentInstance->directory()));
+ } else {
+ MOShared::criticalOnTop(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 RestartExitCode;
+ } 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({});
+
+ // don't reprocess command line
+ m_cl.clear();
}
bool MOApplication::setStyleFile(const QString& styleName)
@@ -112,7 +491,6 @@ bool MOApplication::setStyleFile(const QString& styleName)
return true;
}
-
bool MOApplication::notify(QObject* receiver, QEvent* event)
{
try {
@@ -132,7 +510,6 @@ bool MOApplication::notify(QObject* receiver, QEvent* event)
}
}
-
void MOApplication::updateStyle(const QString& fileName)
{
if (QStyleFactory::keys().contains(fileName)) {
@@ -147,3 +524,74 @@ void MOApplication::updateStyle(const QString& 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;
+}
diff --git a/src/moapplication.h b/src/moapplication.h
index c67c4622..91b8023a 100644
--- a/src/moapplication.h
+++ b/src/moapplication.h
@@ -23,14 +23,26 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QApplication>
#include <QFileSystemWatcher>
+class Settings;
+class MOMultiProcess;
+class Instance;
+class PluginContainer;
+
+namespace MOBase { class IPluginGame; }
+namespace cl { class CommandLine; }
class MOApplication : public QApplication
{
Q_OBJECT
public:
- static MOApplication create(int& argc, char** argv);
- virtual bool notify (QObject* receiver, QEvent* event);
+ MOApplication(cl::CommandLine& cl, int& argc, char** argv);
+
+ // sets up everything, creates the main window and runs it
+ //
+ int run(MOMultiProcess& multiProcess);
+
+ virtual bool notify(QObject* receiver, QEvent* event);
public slots:
bool setStyleFile(const QString& style);
@@ -41,9 +53,32 @@ private slots:
private:
QFileSystemWatcher m_StyleWatcher;
QString m_DefaultStyle;
+ cl::CommandLine& m_cl;
- MOApplication(int& argc, char** argv);
+ int doOneRun(MOMultiProcess& multiProcess);
+
+ std::optional<Instance> getCurrentInstance();
+ std::optional<int> setupInstanceLoop(Instance& currentInstance, PluginContainer& pc);
+ void purgeOldFiles();
+ void resetForRestart();
};
+class MOSplash
+{
+public:
+ MOSplash(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game);
+
+ void close();
+
+private:
+ std::unique_ptr<QSplashScreen> ss_;
+
+ QString getSplashPath(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game) const;
+};
+
#endif // MOAPPLICATION_H
diff --git a/src/singleinstance.cpp b/src/multiprocess.cpp
index bd7ccc43..4ce58718 100644
--- a/src/singleinstance.cpp
+++ b/src/multiprocess.cpp
@@ -1,23 +1,4 @@
-/*
-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 "singleinstance.h"
+#include "multiprocess.h"
#include "utility.h"
#include <report.h>
#include <log.h>
@@ -28,7 +9,7 @@ static const int s_Timeout = 5000;
using MOBase::reportError;
-SingleInstance::SingleInstance(bool allowMultiple, QObject *parent) :
+MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject *parent) :
QObject(parent), m_Ephemeral(false), m_OwnsSM(false)
{
m_SharedMem.setKey(s_Key);
@@ -58,7 +39,7 @@ SingleInstance::SingleInstance(bool allowMultiple, QObject *parent) :
}
-void SingleInstance::sendMessage(const QString &message)
+void MOMultiProcess::sendMessage(const QString &message)
{
if (m_OwnsSM) {
// nobody there to receive the message
@@ -72,19 +53,19 @@ void SingleInstance::sendMessage(const QString &message)
Sleep(250);
}
- // other instance may be just starting up
+ // other process may be just starting up
socket.connectToServer(s_Key, QIODevice::WriteOnly);
connected = socket.waitForConnected(s_Timeout);
}
if (!connected) {
- reportError(tr("failed to connect to running instance: %1").arg(socket.errorString()));
+ reportError(tr("failed to connect to running process: %1").arg(socket.errorString()));
return;
}
socket.write(message.toUtf8());
if (!socket.waitForBytesWritten(s_Timeout)) {
- reportError(tr("failed to communicate with running instance: %1").arg(socket.errorString()));
+ reportError(tr("failed to communicate with running process: %1").arg(socket.errorString()));
return;
}
@@ -92,8 +73,7 @@ void SingleInstance::sendMessage(const QString &message)
socket.waitForDisconnected();
}
-
-void SingleInstance::receiveMessage()
+void MOMultiProcess::receiveMessage()
{
QLocalSocket *socket = m_Server.nextPendingConnection();
if (!socket) {
@@ -108,10 +88,10 @@ void SingleInstance::receiveMessage()
if (av <= 0) {
MOBase::log::error(
- "failed to receive data from secondary instance: {}",
+ "failed to receive data from secondary process: {}",
socket->errorString());
- reportError(tr("failed to receive data from secondary instance: %1").arg(socket->errorString()));
+ reportError(tr("failed to receive data from secondary process: %1").arg(socket->errorString()));
return;
}
}
diff --git a/src/multiprocess.h b/src/multiprocess.h
new file mode 100644
index 00000000..810e63d2
--- /dev/null
+++ b/src/multiprocess.h
@@ -0,0 +1,70 @@
+#ifndef MODORGANIZER_MOMULTIPROCESS_INCLUDED
+#define MODORGANIZER_MOMULTIPROCESS_INCLUDED
+
+#include <QObject>
+#include <QSharedMemory>
+#include <QLocalServer>
+
+
+/**
+ * used to ensure only a single process of Mod Organizer is started and to
+ * allow ephemeral processes to send messages to the primary (visible) one.
+ * This way, other processes can start a download in the primary one
+ **/
+class MOMultiProcess : public QObject
+{
+ Q_OBJECT
+
+public:
+ // `allowMultiple`: if another process is running, run this one
+ // disconnected from the shared memory
+ explicit MOMultiProcess(bool allowMultiple, QObject *parent = 0);
+
+ /**
+ * @return true if this process's job is to forward data to the primary
+ * process through shared memory
+ **/
+ bool ephemeral() const
+ {
+ return m_Ephemeral;
+ }
+
+ // returns true if this is not the primary process, but was allowed because
+ // of the AllowMultiple flag
+ //
+ bool secondary() const
+ {
+ return !m_Ephemeral && !m_OwnsSM;
+ }
+
+ /**
+ * send a message to the primary process. This can be used to transmit download urls
+ *
+ * @param message message to send
+ **/
+ void sendMessage(const QString &message);
+
+signals:
+
+ /**
+ * @brief emitted when an ephemeral process has sent a message (to us)
+ *
+ * @param message the message we received
+ **/
+ void messageSent(const QString &message);
+
+public slots:
+
+private slots:
+
+ void receiveMessage();
+
+private:
+ bool m_Ephemeral;
+ bool m_OwnsSM;
+ QSharedMemory m_SharedMem;
+ QLocalServer m_Server;
+
+};
+
+#endif // MODORGANIZER_MOMULTIPROCESS_INCLUDED
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index d01aab96..f57903e2 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -76,8 +76,7 @@
using namespace MOShared;
using namespace MOBase;
-//static
-CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None;
+static env::CoreDumpTypes g_coreDumpType = env::CoreDumpTypes::Mini;
template <typename InputIterator>
QStringList toStringList(InputIterator current, InputIterator end)
@@ -478,7 +477,7 @@ bool OrganizerCore::bootstrap()
m_Settings.paths().mods(),
m_Settings.paths().downloads(),
m_Settings.paths().overwrite(),
- QString::fromStdWString(crashDumpsPath())
+ QString::fromStdWString(getGlobalCoreDumpPath())
};
for (auto&& dir : dirs) {
@@ -497,14 +496,14 @@ bool OrganizerCore::bootstrap()
// log if there are any dmp files
const auto hasCrashDumps =
- !QDir(QString::fromStdWString(crashDumpsPath()))
+ !QDir(QString::fromStdWString(getGlobalCoreDumpPath()))
.entryList({"*.dmp"}, QDir::Files)
.empty();
if (hasCrashDumps) {
log::debug(
"there are crash dumps in '{}'",
- QString::fromStdWString(crashDumpsPath()));
+ QString::fromStdWString(getGlobalCoreDumpPath()));
}
return true;
@@ -528,13 +527,14 @@ void OrganizerCore::prepareVFS()
}
void OrganizerCore::updateVFSParams(
- log::Levels logLevel, CrashDumpsType crashDumpsType,
+ log::Levels logLevel, env::CoreDumpTypes coreDumpType,
const QString& crashDumpsPath,
std::chrono::seconds spawnDelay, QString executableBlacklist)
{
- setGlobalCrashDumpsType(crashDumpsType);
+ setGlobalCoreDumpType(coreDumpType);
+
m_USVFS.updateParams(
- logLevel, crashDumpsType, crashDumpsPath, spawnDelay, executableBlacklist);
+ logLevel, coreDumpType, crashDumpsPath, spawnDelay, executableBlacklist);
}
void OrganizerCore::setLogLevel(log::Levels level)
@@ -543,8 +543,8 @@ void OrganizerCore::setLogLevel(log::Levels level)
updateVFSParams(
m_Settings.diagnostics().logLevel(),
- m_Settings.diagnostics().crashDumpsType(),
- QString::fromStdWString(crashDumpsPath()),
+ m_Settings.diagnostics().coreDumpType(),
+ QString::fromStdWString(getGlobalCoreDumpPath()),
m_Settings.diagnostics().spawnDelay(),
m_Settings.executablesBlacklist());
@@ -553,8 +553,8 @@ void OrganizerCore::setLogLevel(log::Levels level)
bool OrganizerCore::cycleDiagnostics()
{
- const auto maxDumps = settings().diagnostics().crashDumpsMax();
- const auto path = QString::fromStdWString(crashDumpsPath());
+ const auto maxDumps = settings().diagnostics().maxCoreDumps();
+ const auto path = QString::fromStdWString(getGlobalCoreDumpPath());
if (maxDumps > 0) {
removeOldFiles(path, "*.dmp", maxDumps, QDir::Time|QDir::Reversed);
@@ -563,16 +563,26 @@ bool OrganizerCore::cycleDiagnostics()
return true;
}
-void OrganizerCore::setGlobalCrashDumpsType(CrashDumpsType type)
+env::CoreDumpTypes OrganizerCore::getGlobalCoreDumpType()
+{
+ return g_coreDumpType;
+}
+
+void OrganizerCore::setGlobalCoreDumpType(env::CoreDumpTypes type)
{
- m_globalCrashDumpsType = type;
+ g_coreDumpType = type;
}
-std::wstring OrganizerCore::crashDumpsPath() {
- return (
- qApp->property("dataPath").toString() + "/"
- + QString::fromStdWString(AppConfig::dumpsDir())
- ).toStdWString();
+std::wstring OrganizerCore::getGlobalCoreDumpPath()
+{
+ if (qApp) {
+ const auto dp = qApp->property("dataPath");
+ if (!dp.isNull()) {
+ return dp.toString().toStdWString() + L"/" + AppConfig::dumpsDir();
+ }
+ }
+
+ return {};
}
void OrganizerCore::setCurrentProfile(const QString &profileName)
diff --git a/src/organizercore.h b/src/organizercore.h
index 80b89a43..b566e626 100644
--- a/src/organizercore.h
+++ b/src/organizercore.h
@@ -13,6 +13,7 @@
#include "moshortcut.h"
#include "processrunner.h"
#include "uilocker.h"
+#include "envdump.h"
#include <imoinfo.h>
#include <iplugindiagnose.h>
#include <versioninfo.h>
@@ -277,17 +278,17 @@ public:
void prepareVFS();
void updateVFSParams(
- MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType,
- const QString& crashDumpsPath, std::chrono::seconds spawnDelay,
+ MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType,
+ const QString& coreDumpsPath, std::chrono::seconds spawnDelay,
QString executableBlacklist);
void setLogLevel(MOBase::log::Levels level);
bool cycleDiagnostics();
- static CrashDumpsType getGlobalCrashDumpsType() { return m_globalCrashDumpsType; }
- static void setGlobalCrashDumpsType(CrashDumpsType crashDumpsType);
- static std::wstring crashDumpsPath();
+ static env::CoreDumpTypes getGlobalCoreDumpType();
+ static void setGlobalCoreDumpType(env::CoreDumpTypes type);
+ static std::wstring getGlobalCoreDumpPath();
/**
* @brief Returns the name of all the mods in the priority order of the given profile.
@@ -487,8 +488,6 @@ private:
UsvfsConnector m_USVFS;
UILocker m_UILocker;
-
- static CrashDumpsType m_globalCrashDumpsType;
};
#endif // ORGANIZERCORE_H
diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp
index 908677c2..dd4ccbb1 100644
--- a/src/plugincontainer.cpp
+++ b/src/plugincontainer.cpp
@@ -405,22 +405,58 @@ void PluginContainer::loadPlugins()
}
QFile loadCheck;
+ QString skipPlugin;
if (m_Organizer) {
loadCheck.setFileName(qApp->property("dataPath").toString() + "/plugin_loadcheck.tmp");
+
if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) {
// oh, there was a failed plugin load last time. Find out which plugin was loaded last
QString fileName;
while (!loadCheck.atEnd()) {
fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed();
}
- if (QMessageBox::question(nullptr, QObject::tr("Plugin error"),
- QObject::tr("It appears the plugin \"%1\" failed to load last startup and caused MO to crash. Do you want to disable it?\n"
- "(Please note: If this is the first time you see this message for this plugin you may want to give it another try. "
- "The plugin may be able to recover from the problem)").arg(fileName),
- QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) {
- m_Organizer->settings().plugins().addBlacklist(fileName);
+
+ log::warn("loadcheck file found for plugin '{}'", fileName);
+
+ MOBase::TaskDialog dlg;
+
+ const auto Skip = QMessageBox::Ignore;
+ const auto Blacklist = QMessageBox::Cancel;
+ const auto Load = QMessageBox::Ok;
+
+ const auto r = dlg
+ .title(tr("Plugin error"))
+ .main(tr(
+ "Mod Organizer failed to load the plugin '%1' last time it was started.")
+ .arg(fileName))
+ .content(tr(
+ "The plugin can be skipped for this session, blacklisted, "
+ "or loaded normally, in which case it might fail again. Blacklisted "
+ "plugins can be re-enabled later in the settings."))
+ .icon(QMessageBox::Warning)
+ .button({tr("Skip this plugin"), Skip})
+ .button({tr("Blacklist this plugin"), Blacklist})
+ .button({tr("Load this plugin"), Load})
+ .exec();
+
+ switch (r)
+ {
+ case Skip:
+ log::warn("user wants to skip plugin '{}'", fileName);
+ skipPlugin = fileName;
+ break;
+
+ case Blacklist:
+ log::warn("user wants to blacklist plugin '{}'", fileName);
+ m_Organizer->settings().plugins().addBlacklist(fileName);
+ break;
+
+ case Load:
+ log::warn("user wants to load plugin '{}' anyway", fileName);
+ break;
}
+
loadCheck.close();
}
@@ -434,6 +470,11 @@ void PluginContainer::loadPlugins()
while (iter.hasNext()) {
iter.next();
+ if (skipPlugin == iter.fileName()) {
+ log::debug("plugin \"{}\" skipped for this session", iter.fileName());
+ continue;
+ }
+
if (m_Organizer) {
if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) {
log::debug("plugin \"{}\" blacklisted", iter.fileName());
@@ -467,11 +508,25 @@ void PluginContainer::loadPlugins()
}
}
- // remove the load check file on success
- if (loadCheck.isOpen()) {
- loadCheck.remove();
+ if (skipPlugin.isEmpty()) {
+ // remove the load check file on success
+ if (loadCheck.isOpen()) {
+ loadCheck.remove();
+ }
+ } else {
+ // remember the plugin for next time
+ if (loadCheck.isOpen()) {
+ loadCheck.close();
+ }
+
+ log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin);
+ loadCheck.open(QIODevice::WriteOnly);
+ loadCheck.write(skipPlugin.toUtf8());
+ loadCheck.write("\n");
+ loadCheck.flush();
}
+
bf::at_key<IPluginDiagnose>(m_Plugins).push_back(this);
if (m_Organizer) {
diff --git a/src/sanitychecks.cpp b/src/sanitychecks.cpp
index 7afce7bd..613e4b26 100644
--- a/src/sanitychecks.cpp
+++ b/src/sanitychecks.cpp
@@ -1,3 +1,4 @@
+#include "sanitychecks.h"
#include "env.h"
#include "envmodule.h"
#include "settings.h"
@@ -5,6 +6,9 @@
#include <log.h>
#include <utility.h>
+namespace sanity
+{
+
using namespace MOBase;
enum class SecurityZone
@@ -368,7 +372,7 @@ int checkProtected(const QDir& d, const QString& what)
return 0;
}
-int checkPathsForSanity(IPluginGame& game, const Settings& s)
+int checkPaths(IPluginGame& game, const Settings& s)
{
log::debug("checking paths");
@@ -390,7 +394,7 @@ int checkPathsForSanity(IPluginGame& game, const Settings& s)
return n;
}
-void sanityChecks(const env::Environment& e)
+void checkEnvironment(const env::Environment& e)
{
log::debug("running sanity checks...");
@@ -404,3 +408,5 @@ void sanityChecks(const env::Environment& e)
"sanity checks done, {}",
(n > 0 ? "problems were found" : "everything looks okay"));
}
+
+} // namespace
diff --git a/src/sanitychecks.h b/src/sanitychecks.h
new file mode 100644
index 00000000..8786ee78
--- /dev/null
+++ b/src/sanitychecks.h
@@ -0,0 +1,27 @@
+#ifndef MODORGANIZER_SANITYCHECKS_INCLUDED
+#define MODORGANIZER_SANITYCHECKS_INCLUDED
+
+namespace env
+{
+ class Environment;
+ class Module;
+}
+
+namespace MOBase
+{
+ class IPluginGame;
+}
+
+class Settings;
+
+
+namespace sanity
+{
+
+void checkEnvironment(const env::Environment& env);
+int checkIncompatibleModule(const env::Module& m);
+int checkPaths(MOBase::IPluginGame& game, const Settings& s);
+
+} // namespace
+
+#endif // MODORGANIZER_SANITYCHECKS_INCLUDED
diff --git a/src/settings.cpp b/src/settings.cpp
index 99b41e1f..54d32786 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -86,14 +86,20 @@ Settings::~Settings()
}
}
-Settings &Settings::instance()
+Settings& Settings::instance()
{
if (s_Instance == nullptr) {
throw std::runtime_error("no instance of \"Settings\"");
}
+
return *s_Instance;
}
+Settings* Settings::maybeInstance()
+{
+ return s_Instance;
+}
+
void Settings::processUpdates(
const QVersionNumber& currentVersion, const QVersionNumber& lastVersion)
{
@@ -2110,23 +2116,23 @@ void DiagnosticsSettings::setLootLogLevel(lootcli::LogLevels level)
set(m_Settings, "Settings", "loot_log_level", level);
}
-CrashDumpsType DiagnosticsSettings::crashDumpsType() const
+env::CoreDumpTypes DiagnosticsSettings::coreDumpType() const
{
- return get<CrashDumpsType>(m_Settings,
- "Settings", "crash_dumps_type", CrashDumpsType::Mini);
+ return get<env::CoreDumpTypes>(m_Settings,
+ "Settings", "crash_dumps_type", env::CoreDumpTypes::Mini);
}
-void DiagnosticsSettings::setCrashDumpsType(CrashDumpsType type)
+void DiagnosticsSettings::setCoreDumpType(env::CoreDumpTypes type)
{
set(m_Settings, "Settings", "crash_dumps_type", type);
}
-int DiagnosticsSettings::crashDumpsMax() const
+int DiagnosticsSettings::maxCoreDumps() const
{
return get<int>(m_Settings, "Settings", "crash_dumps_max", 5);
}
-void DiagnosticsSettings::setCrashDumpsMax(int n)
+void DiagnosticsSettings::setMaxCoreDumps(int n)
{
set(m_Settings, "Settings", "crash_dumps_max", n);
}
diff --git a/src/settings.h b/src/settings.h
index c8325ba2..689067d7 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#define SETTINGS_H
#include "loadmechanism.h"
+#include "envdump.h"
#include <filterwidget.h>
#include <lootcli/lootcli.h>
#include <questionboxmemory.h>
@@ -195,6 +196,10 @@ private:
class WidgetSettings
{
public:
+ // globalInstance is forwarded from the Settings constructor; WidgetSettings
+ // has the callbacks used by QuestionBoxMemory and those are global, so they
+ // should only be set by the global instance
+ //
WidgetSettings(QSettings& s, bool globalInstance);
// selected index for a combobox
@@ -647,13 +652,13 @@ public:
// crash dump type for both MO and usvfs
//
- CrashDumpsType crashDumpsType() const;
- void setCrashDumpsType(CrashDumpsType type);
+ env::CoreDumpTypes coreDumpType() const;
+ void setCoreDumpType(env::CoreDumpTypes type);
// maximum number of dump files keps, for both MO and usvfs
//
- int crashDumpsMax() const;
- void setCrashDumpsMax(int n);
+ int maxCoreDumps() const;
+ void setMaxCoreDumps(int n);
std::chrono::seconds spawnDelay() const;
void setSpawnDelay(std::chrono::seconds t);
@@ -671,10 +676,32 @@ class Settings : public QObject
Q_OBJECT;
public:
+ // there is one Settings global object for MO when an instance is loaded, but
+ // other Settings objects are required in several places, such as in the
+ // Instance class, the instance dialogs, etc.
+ //
+ // any Settings object created with globalInstance==false won't set the
+ // singleton
+ //
+ // only WidgetSettings need to know whether it created from a globalInstance,
+ // see its constructor
+ //
+ // @param path path to an ini file
+ // @param globalInsance whether this is the global instance; creates the
+ // singleton and asserts if it already exists
+ //
Settings(const QString& path, bool globalInstance=false);
~Settings();
- static Settings &instance();
+
+ // throws if there is no global Settings instance
+ //
+ static Settings& instance();
+
+ // returns null if there is no global Settings instance
+ //
+ static Settings* maybeInstance();
+
// name of the ini file
//
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index 33c1c2ee..85c5a4e4 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -1044,9 +1044,6 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
- <property name="handleWidth">
- <number>2</number>
- </property>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
@@ -1055,6 +1052,18 @@
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_6">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
<item>
<widget class="QSplitter" name="splitter_2">
<property name="sizePolicy">
@@ -1066,11 +1075,20 @@
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
- <property name="handleWidth">
- <number>2</number>
- </property>
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_15">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
<item>
<widget class="QTreeWidget" name="pluginsList">
<column>
@@ -1095,6 +1113,18 @@
</widget>
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_7" stretch="0,1">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
@@ -1182,22 +1212,18 @@
</item>
</layout>
</widget>
- <widget class="QWidget" name="verticalWidget" native="true">
+ <widget class="QGroupBox" name="verticalGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
+ <property name="title">
+ <string>Blacklisted Plugins (use &lt;del&gt; to remove):</string>
+ </property>
<layout class="QVBoxLayout" name="verticalLayout_16">
<item>
- <widget class="QLabel" name="label_18">
- <property name="text">
- <string>Blacklisted Plugins (use &lt;del&gt; to remove):</string>
- </property>
- </widget>
- </item>
- <item>
<widget class="QListWidget" name="pluginBlacklist"/>
</item>
</layout>
@@ -1614,15 +1640,15 @@ programs you are intentionally running.</string>
</widget>
<customwidgets>
<customwidget>
- <class>ColorTable</class>
- <extends>QTableWidget</extends>
- <header>colortable.h</header>
- </customwidget>
- <customwidget>
<class>LinkLabel</class>
<extends>QLabel</extends>
<header location="global">linklabel.h</header>
</customwidget>
+ <customwidget>
+ <class>ColorTable</class>
+ <extends>QTableWidget</extends>
+ <header>colortable.h</header>
+ </customwidget>
</customwidgets>
<tabstops>
<tabstop>tabWidget</tabstop>
diff --git a/src/settingsdialogdiagnostics.cpp b/src/settingsdialogdiagnostics.cpp
index 4ade6900..33175a66 100644
--- a/src/settingsdialogdiagnostics.cpp
+++ b/src/settingsdialogdiagnostics.cpp
@@ -13,7 +13,7 @@ DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d)
setLootLogLevel();
setCrashDumpTypesBox();
- ui->dumpsMaxEdit->setValue(settings().diagnostics().crashDumpsMax());
+ ui->dumpsMaxEdit->setValue(settings().diagnostics().maxCoreDumps());
QString logsPath = qApp->property("dataPath").toString()
+ "/" + QString::fromStdWString(AppConfig::logPath());
@@ -22,7 +22,7 @@ DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d)
ui->diagnosticsExplainedLabel->text()
.replace("LOGS_FULL_PATH", logsPath)
.replace("LOGS_DIR", QString::fromStdWString(AppConfig::logPath()))
- .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::crashDumpsPath()))
+ .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::getGlobalCoreDumpPath()))
.replace("DUMPS_DIR", QString::fromStdWString(AppConfig::dumpsDir()))
);
}
@@ -78,13 +78,13 @@ void DiagnosticsSettingsTab::setCrashDumpTypesBox()
ui->dumpsTypeBox->addItem(text, static_cast<int>(type));
};
- add(QObject::tr("None"), CrashDumpsType::None);
- add(QObject::tr("Mini (recommended)"), CrashDumpsType::Mini);
- add(QObject::tr("Data"), CrashDumpsType::Data);
- add(QObject::tr("Full"), CrashDumpsType::Full);
+ add(QObject::tr("None"), env::CoreDumpTypes::None);
+ add(QObject::tr("Mini (recommended)"), env::CoreDumpTypes::Mini);
+ add(QObject::tr("Data"), env::CoreDumpTypes::Data);
+ add(QObject::tr("Full"), env::CoreDumpTypes::Full);
const auto current = static_cast<int>(
- settings().diagnostics().crashDumpsType());
+ settings().diagnostics().coreDumpType());
for (int i=0; i<ui->dumpsTypeBox->count(); ++i) {
if (ui->dumpsTypeBox->itemData(i) == current) {
@@ -99,10 +99,10 @@ void DiagnosticsSettingsTab::update()
settings().diagnostics().setLogLevel(
static_cast<log::Levels>(ui->logLevelBox->currentData().toInt()));
- settings().diagnostics().setCrashDumpsType(
- static_cast<CrashDumpsType>(ui->dumpsTypeBox->currentData().toInt()));
+ settings().diagnostics().setCoreDumpType(
+ static_cast<env::CoreDumpTypes>(ui->dumpsTypeBox->currentData().toInt()));
- settings().diagnostics().setCrashDumpsMax(ui->dumpsMaxEdit->value());
+ settings().diagnostics().setMaxCoreDumps(ui->dumpsMaxEdit->value());
settings().diagnostics().setLootLogLevel(
static_cast<lootcli::LogLevels>(ui->lootLogLevel->currentData().toInt()));
diff --git a/src/shared/appconfig.inc b/src/shared/appconfig.inc
index 807f1d69..5839c2f4 100644
--- a/src/shared/appconfig.inc
+++ b/src/shared/appconfig.inc
@@ -11,7 +11,7 @@ APPPARAM(std::wstring, logPath, L"logs")
APPPARAM(std::wstring, dumpsDir, L"crashDumps")
APPPARAM(std::wstring, defaultProfileName, L"Default")
APPPARAM(std::wstring, profileTweakIni, L"profile_tweaks.ini")
-APPPARAM(std::wstring, logFileName, L"ModOrganizer.log")
+APPPARAM(std::wstring, logFileName, L"mo_interface.log")
APPPARAM(std::wstring, iniFileName, L"ModOrganizer.ini")
APPPARAM(std::wstring, proxyDLLTarget, L"steam_api.dll")
APPPARAM(std::wstring, proxyDLLOrig, L"steam_api_orig.dll") // needs to be identical to the value used in proxydll-project
diff --git a/src/shared/error_report.cpp b/src/shared/error_report.cpp
index 4185b544..09fdcb49 100644
--- a/src/shared/error_report.cpp
+++ b/src/shared/error_report.cpp
@@ -51,4 +51,14 @@ void reportError(LPCWSTR format, ...)
MessageBoxW(nullptr, buffer, L"Error", MB_OK | MB_ICONERROR);
}
+void criticalOnTop(const QString& message)
+{
+ QMessageBox mb(QMessageBox::Critical, QObject::tr("Mod Organizer"), message);
+
+ mb.show();
+ mb.activateWindow();
+ mb.raise();
+ mb.exec();
+}
+
} // namespace MOShared
diff --git a/src/shared/error_report.h b/src/shared/error_report.h
index da07c728..9343d3da 100644
--- a/src/shared/error_report.h
+++ b/src/shared/error_report.h
@@ -31,6 +31,14 @@ namespace MOShared
void reportError(LPCSTR format, ...);
void reportError(LPCWSTR format, ...);
+// shows a critical message box that's raised to the top of the zorder, useful
+// for messages without a main window, which sometimes makes them pop up behind
+// all other windows
+//
+// the dialog is not topmost, it's just raised once when shown
+//
+void criticalOnTop(const QString& message);
+
} // namespace MOShared
#endif // MODORGANIZER_SHARED_ERROR_REPORT_INCLUDED
diff --git a/src/singleinstance.h b/src/singleinstance.h
deleted file mode 100644
index 5f6c3633..00000000
--- a/src/singleinstance.h
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
-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/>.
-*/
-
-#ifndef SINGLEINSTANCE_H
-#define SINGLEINSTANCE_H
-
-#include <QObject>
-#include <QSharedMemory>
-#include <QLocalServer>
-
-
-/**
- * used to ensure only a single instance of Mod Organizer is started and to
- * allow ephemeral instances to send messages to the primary (visible) one.
- * This way, other instances can start a download in the primary one
- **/
-class SingleInstance : public QObject
-{
-
- Q_OBJECT
-
-public:
- // `allowMultiple`: if another instance is running, run this one
- // disconnected from the shared memory
- explicit SingleInstance(bool allowMultiple, QObject *parent = 0);
-
- /**
- * @return true if this instance's job is to forward data to the primary
- * instance through shared memory
- **/
- bool ephemeral() const
- {
- return m_Ephemeral;
- }
-
- // returns true if this is not the primary instance, but was allowed because
- // of the AllowMultiple flag
- //
- bool secondary() const
- {
- return !m_Ephemeral && !m_OwnsSM;
- }
-
- /**
- * send a message to the primary instance. This can be used to transmit download urls
- *
- * @param message message to send
- **/
- void sendMessage(const QString &message);
-
-signals:
-
- /**
- * @brief emitted when an ephemeral instance has sent a message (to us)
- *
- * @param message the message we received
- **/
- void messageSent(const QString &message);
-
-public slots:
-
-private slots:
-
- void receiveMessage();
-
-private:
- bool m_Ephemeral;
- bool m_OwnsSM;
- QSharedMemory m_SharedMem;
- QLocalServer m_Server;
-
-};
-
-#endif // SINGLEINSTANCE_H
diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp
index ed6e31ce..c8f2f1c4 100644
--- a/src/usvfsconnector.cpp
+++ b/src/usvfsconnector.cpp
@@ -107,17 +107,22 @@ LogLevel toUsvfsLogLevel(log::Levels level)
}
}
-CrashDumpsType crashDumpsType(int type)
+CrashDumpsType toUsvfsCrashDumpsType(env::CoreDumpTypes type)
{
- switch (static_cast<CrashDumpsType>(type)) {
- case CrashDumpsType::Mini:
- return CrashDumpsType::Mini;
- case CrashDumpsType::Data:
- return CrashDumpsType::Data;
- case CrashDumpsType::Full:
- return CrashDumpsType::Full;
- default:
- return CrashDumpsType::None;
+ switch (type)
+ {
+ case env::CoreDumpTypes::None:
+ return CrashDumpsType::None;
+
+ case env::CoreDumpTypes::Data:
+ return CrashDumpsType::Data;
+
+ case env::CoreDumpTypes::Full:
+ return CrashDumpsType::Full;
+
+ case env::CoreDumpTypes::Mini:
+ default:
+ return CrashDumpsType::Mini;
}
}
@@ -128,9 +133,9 @@ UsvfsConnector::UsvfsConnector()
const auto& s = Settings::instance();
const LogLevel logLevel = toUsvfsLogLevel(s.diagnostics().logLevel());
- const CrashDumpsType dumpType = s.diagnostics().crashDumpsType();
+ const auto dumpType = toUsvfsCrashDumpsType(s.diagnostics().coreDumpType());
const auto delay = duration_cast<milliseconds>(s.diagnostics().spawnDelay());
- std::string dumpPath = MOShared::ToString(OrganizerCore::crashDumpsPath(), true);
+ std::string dumpPath = MOShared::ToString(OrganizerCore::getGlobalCoreDumpPath(), true);
usvfsParameters* params = usvfsCreateParameters();
@@ -224,7 +229,7 @@ void UsvfsConnector::updateMapping(const MappingType &mapping)
}
void UsvfsConnector::updateParams(
- MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType,
+ MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType,
const QString& crashDumpsPath, std::chrono::seconds spawnDelay,
QString executableBlacklist)
{
@@ -234,7 +239,7 @@ void UsvfsConnector::updateParams(
usvfsSetDebugMode(p, FALSE);
usvfsSetLogLevel(p, toUsvfsLogLevel(logLevel));
- usvfsSetCrashDumpType(p, crashDumpsType);
+ usvfsSetCrashDumpType(p, toUsvfsCrashDumpsType(coreDumpType));
usvfsSetCrashDumpPath(p, crashDumpsPath.toStdString().c_str());
usvfsSetProcessDelay(p, duration_cast<milliseconds>(spawnDelay).count());
diff --git a/src/usvfsconnector.h b/src/usvfsconnector.h
index 5982778b..bf5d49ce 100644
--- a/src/usvfsconnector.h
+++ b/src/usvfsconnector.h
@@ -31,6 +31,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <usvfsparameters.h>
#include <log.h>
#include "executableinfo.h"
+#include "envdump.h"
class LogWorker : public QThread {
@@ -87,7 +88,7 @@ public:
void updateMapping(const MappingType &mapping);
void updateParams(
- MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType,
+ MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType,
const QString& crashDumpsPath, std::chrono::seconds spawnDelay,
QString executableBlacklist);