diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2020-11-06 10:51:03 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-11-06 10:51:03 -0500 |
| commit | b909677c3fc6a7b7a1993d341a2bd420715e292a (patch) | |
| tree | 8a883011d5e207c31edf9b94ee67bff1827c2ddf /src/instancemanager.cpp | |
| parent | a63a27840bfa7b08f295bfe682ebc33d70a613b9 (diff) | |
| parent | ad8e9d99b30578676c15d10a74f010439e132406 (diff) | |
Merge pull request #1280 from isanae/command-line
Command line and instance dialogs
Diffstat (limited to 'src/instancemanager.cpp')
| -rw-r--r-- | src/instancemanager.cpp | 554 |
1 files changed, 354 insertions, 200 deletions
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index 6c7f797e..8163149b 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -20,9 +20,15 @@ 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 "shared/util.h" +#include <report.h> +#include <iplugingame.h> #include <utility.h> #include <log.h> -#include "shared/appconfig.h" + #include <QCoreApplication> #include <QDir> #include <QStandardPaths> @@ -32,258 +38,375 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase; -static const char COMPANY_NAME[] = "Tannin"; -static const char APPLICATION_NAME[] = "Mod Organizer"; -static const char INSTANCE_KEY[] = "CurrentInstance"; +Instance::Instance(QDir dir, bool portable, QString profileName) : + m_dir(std::move(dir)), m_portable(portable), m_plugin(nullptr), + m_profile(std::move(profileName)) +{ +} +QString Instance::name() const +{ + if (isPortable()) + return QObject::tr("Portable"); + else + return m_dir.dirName(); +} -InstanceManager::InstanceManager() - : m_AppSettings(COMPANY_NAME, APPLICATION_NAME) +QString Instance::gameName() const { + return m_gameName; } -InstanceManager &InstanceManager::instance() +QString Instance::gameDirectory() const { - static InstanceManager s_Instance; - return s_Instance; + return m_gameDir; } -void InstanceManager::overrideInstance(const QString& instanceName) +QDir Instance::directory() const { - m_overrideInstanceName = instanceName; - m_overrideInstance = true; + return m_dir; } +MOBase::IPluginGame* Instance::gamePlugin() const +{ + return m_plugin; +} -QString InstanceManager::currentInstance() const +QString Instance::profileName() const { - if (m_overrideInstance) - return m_overrideInstanceName; - else - return m_AppSettings.value(INSTANCE_KEY, "").toString(); + return m_profile; } -void InstanceManager::clearCurrentInstance() +QString Instance::iniPath() const { - setCurrentInstance(""); - m_Reset = true; - m_overrideInstance = false; + return InstanceManager::iniPath(m_dir); } -void InstanceManager::setCurrentInstance(const QString &name) +bool Instance::isPortable() const { - m_AppSettings.setValue(INSTANCE_KEY, name); + return m_portable; } -bool InstanceManager::deleteLocalInstance(const QString &instanceId) const +Instance::SetupResults Instance::setup(PluginContainer& plugins) { - bool result = true; - QString instancePath = QDir::fromNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + instanceId); + Settings s(iniPath()); - if (QMessageBox::warning(nullptr, QObject::tr("Deleting folder"), - QObject::tr("I'm about to delete the following folder: \"%1\". Proceed?").arg(instancePath), QMessageBox::No | QMessageBox::Yes, QMessageBox::No) == QMessageBox::No ){ - return false; + if (s.iniStatus() != QSettings::NoError) { + log::error("can't read ini {}", iniPath()); + return SetupResults::BadIni; } - if (!MOBase::shellDelete(QStringList(instancePath),true)) - { - log::warn( - "Failed to shell-delete \"{}\" (errorcode {}), trying regular delete", - instancePath, ::GetLastError()); + if (m_gameName.isEmpty()) { + if (auto v=s.game().name()) + m_gameName = *v; + } - if (!MOBase::removeDir(instancePath)) - { - log::warn("regular delete failed too"); - result = false; + if (m_gameDir.isEmpty()) { + if (auto v=s.game().directory()) + 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; } } - return result; + if (m_gameVariant.isEmpty() && m_plugin->gameVariants().size() > 1) { + return SetupResults::MissingVariant; + } else { + m_plugin->setGameVariant(m_gameVariant); + } + + getProfile(s); + + s.game().setName(m_gameName); + s.game().setDirectory(m_gameDir); + s.game().setSelectedProfileName(m_profile); + + if (!m_gameVariant.isEmpty()) + s.game().setEdition(m_gameVariant); + + m_plugin->setGamePath(m_gameDir); + + return SetupResults::Ok; } -QString InstanceManager::manageInstances(const QStringList &instanceList) const +void Instance::setGame(const QString& name, const QString& dir) { - SelectionDialog selection( - QString("<h3>%1</h3><br>%2") - .arg(QObject::tr("Choose Instance to Delete")) - .arg(QObject::tr("Be Careful! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.")), - nullptr); - for (const QString &instance : instanceList) - { - selection.addChoice(QIcon(":/MO/gui/multiply_red"), instance, "", instance); - } + m_gameName = name; + m_gameDir = dir; +} - if (selection.exec() == QDialog::Rejected) { - return(chooseInstance(instances())); - } - else { - QString choice = selection.getChoiceData().toString(); - { - if (QMessageBox::warning(nullptr, QObject::tr("Are you sure?"), - QObject::tr("Are you really sure you want to delete the Instance \"%1\" with all its files?").arg(choice), QMessageBox::No | QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) - { - if (!deleteLocalInstance(choice)) - { - QMessageBox::warning(nullptr, QObject::tr("Failed to delete Instance"), - QObject::tr("Could not delete Instance \"%1\". \nIf the folder was still in use, restart MO and try again.").arg(choice), QMessageBox::Ok); - } - } - } - } - return(manageInstances(instances())); +void Instance::setVariant(const QString& name) +{ + m_gameVariant = name; } -QString InstanceManager::queryInstanceName(const QStringList &instanceList) const +Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) { - QString instanceId; - QString dialogText; - while (instanceId.isEmpty()) { - QInputDialog dialog; + if (!m_gameName.isEmpty() && !m_gameDir.isEmpty()) + { + // normal case: both the name and dir are in the ini - dialog.setWindowTitle(QObject::tr("Enter a Name for the new Instance")); - dialog.setLabelText(QObject::tr("Enter a new name or select one from the suggested list: \n" - "(This is just a name for the Instance and can be whatever you wish,\n" - " the actual game selection will happen on the next screen regardless of chosen name)")); - // would be neat if we could take the names from the game plugins but - // the required initialization order requires the ini file to be - // available *before* we load plugins - dialog.setComboBoxItems({ "NewName", "Fallout 4", "SkyrimSE", "Skyrim", "SkyrimVR", "Fallout 3", - "Fallout NV", "TTW", "FO4VR", "Oblivion", "Morrowind", "Enderal" }); - dialog.setComboBoxEditable(true); + // find the plugin by name + for (IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (m_gameName.compare(game->gameName(), Qt::CaseInsensitive) == 0) { + // plugin found, check if the game directory is valid - if (dialog.exec() == QDialog::Rejected) { - throw MOBase::MyException(QObject::tr("Canceled")); - } - dialogText = dialog.textValue(); - instanceId = sanitizeInstanceName(dialogText); - if (instanceId != dialogText) { - if (QMessageBox::question( nullptr, - QObject::tr("Invalid instance name"), - QObject::tr("The instance name \"%1\" is invalid. Use the name \"%2\" instead?").arg(dialogText,instanceId), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { - instanceId=""; - continue; + if (!game->looksValid(m_gameDir)) { + // the directory from the ini is not valid anymore + log::warn( + "game plugin {} says dir {} from ini {} is not valid", + game->gameName(), m_gameDir, iniPath()); + + // note that some plugins return true for isInstalled() if a path + // is found in the registry, but without actually checking if it's + // valid + + if (game->isInstalled() && game->looksValid(game->gameDirectory())) { + // bad game directory but the plugin reports there's a valid one + // somewhere; take it instead + log::warn( + "game plugin {} found a game at {}, taking it", + game->gameName(), game->gameDirectory().absolutePath()); + + m_gameDir = game->gameDirectory().absolutePath(); + } else { + // game seems to be gone completely + log::warn("game plugin {} found no game installation at all", game->gameName()); + return SetupResults::GameGone; + } } + + m_plugin = game; + return SetupResults::Ok; + } } - bool alreadyExists=false; - for (const QString &instance : instanceList) { - if(instanceId==instance) - alreadyExists=true; + log::warn("game plugin {} not found", m_gameName); + return SetupResults::PluginGone; + } + else if (m_gameName.isEmpty() && !m_gameDir.isEmpty()) + { + // the name is missing, but there's a directory; find a plugin that can + // handle it + + log::warn( + "game name is missing from ini {} but dir {} is available", + iniPath(), m_gameDir); + + for (IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (game->looksValid(m_gameDir)) { + // take it + log::warn("found plugin {} that can use dir {}", game->gameName(), m_gameDir); + + m_plugin = game; + m_gameName = game->gameName(); + + return SetupResults::Ok; + } } - if(alreadyExists) - { - QMessageBox msgBox; - msgBox.setText( QObject::tr("The instance \"%1\" already exists.").arg(instanceId) ); - msgBox.setInformativeText(QObject::tr("Please choose a different instance name, like: \"%1 1\" .").arg(instanceId)); - msgBox.exec(); - instanceId=""; + + log::error("no plugins can use dir {}", m_gameDir); + return SetupResults::GameGone; + } + else if (!m_gameName.isEmpty() && m_gameDir.isEmpty()) + { + // dir is missing, find a plugin with the correct name and use the install + // dir it detected + + log::warn( + "game dir is missing from ini {} but name {} is available", + iniPath(), m_gameName); + + for (IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (m_gameName.compare(game->gameName(), Qt::CaseInsensitive) == 0) { + // plugin found, use its detected installation dir + + if (game->isInstalled()) { + log::warn( + "found plugin {} that matches name in ini {}, using auto detected " + "game dir {}", + game->gameName(), iniPath(), game->gameDirectory().absolutePath()); + + m_plugin = game; + m_gameDir = game->gameDirectory().absolutePath(); + + return SetupResults::Ok; + } else { + log::warn( + "found plugin {} that matches name in ini {}, but no game install " + "detected by plugin", + game->gameName(), iniPath()); + + return SetupResults::GameGone; + } + } } + + // plugin seems to be gone + log::error("no plugin matches name {}", m_gameName); + return SetupResults::PluginGone; + } + else + { + // can't do anything with these two missing + log::error("both game name and dir are missing from ini {}", iniPath()); + return SetupResults::IniMissingGame; } - return instanceId; } -QString InstanceManager::chooseInstance(const QStringList &instanceList) const +void Instance::getProfile(const Settings& s) { - if (portableInstallIsLocked()) { - return QString(); + if (!m_profile.isEmpty()) { + // there's already a profile set up, probably an override + return; } - enum class Special : uint8_t { - NewInstance, - Portable, - Manage - }; - - SelectionDialog selection( - QString("<h3>%1</h3><br>%2") - .arg(QObject::tr("Choose Instance")) - .arg(QObject::tr( - "Each Instance is a full set of MO data files (mods, " - "downloads, profiles, configuration, ...). You can use multiple " - "instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. " - "If your MO folder is writable, you can also store a single instance locally (called " - "a Portable install, and all the MO data files will be inside the installation folder).")), - nullptr); - selection.disableCancel(); - for (const QString &instance : instanceList) { - selection.addChoice(instance, "", instance); + if (auto name=s.game().selectedProfileName()) { + // use last profile + m_profile = *name; + return; } - selection.addChoice(QIcon(":/MO/gui/add"), QObject::tr("New"), - QObject::tr("Create a new instance."), - static_cast<uint8_t>(Special::NewInstance)); + // profile missing from ini, use the default + m_profile = QString::fromStdWString(AppConfig::defaultProfileName()); - if (QFileInfo(qApp->applicationDirPath()).isWritable()) { - selection.addChoice(QIcon(":/MO/gui/package"), QObject::tr("Portable"), - QObject::tr("Use MO folder for data."), - static_cast<uint8_t>(Special::Portable)); - } + log::warn( + "no profile found in ini {}, using default '{}'", + iniPath(), m_profile); +} + + +InstanceManager::InstanceManager() +{ + GlobalSettings::updateRegistryKey(); +} + +InstanceManager &InstanceManager::singleton() +{ + static InstanceManager s_Instance; + return s_Instance; +} - selection.addChoice(QIcon(":/MO/gui/remove"), QObject::tr("Manage Instances"), - QObject::tr("Delete an Instance."), - static_cast<uint8_t>(Special::Manage)); +void InstanceManager::overrideInstance(const QString& instanceName) +{ + m_overrideInstanceName = instanceName; + m_overrideInstance = true; +} - selection.setWindowFlags(selection.windowFlags() | Qt::WindowStaysOnTopHint); +void InstanceManager::overrideProfile(const QString& profileName) +{ + m_overrideProfileName = profileName; + m_overrideProfile = true; +} - if (selection.exec() == QDialog::Rejected) { - log::debug("rejected"); - throw MOBase::MyException(QObject::tr("Canceled")); +std::optional<Instance> InstanceManager::currentInstance() const +{ + const QString profile = m_overrideProfile ? m_overrideProfileName : ""; + + if (portableInstallIsLocked()) { + // force portable instance + return Instance(QDir(portablePath()), true, profile); } - QVariant choice = selection.getChoiceData(); + QString name; - if (choice.type() == QVariant::String) { - return choice.toString(); - } else { - switch (static_cast<Special>(choice.value<uint8_t>())) { - case Special::NewInstance: return queryInstanceName(instanceList); - case Special::Portable: return QString(); - case Special::Manage: { + if (m_overrideInstance) + name = m_overrideInstanceName; + else + name = GlobalSettings::currentInstance(); - return(manageInstances(instances())); - } - default: throw std::runtime_error("invalid selection"); + if (name.isEmpty()) { + if (portableInstanceExists()) { + // use portable + return Instance(QDir(portablePath()), true, profile); + } else { + // no instance set + return {}; } } + + return Instance(QDir(instancePath(name)), false, profile); } +void InstanceManager::clearCurrentInstance() +{ + setCurrentInstance(""); + m_overrideInstance = false; +} + +void InstanceManager::setCurrentInstance(const QString &name) +{ + GlobalSettings::setCurrentInstance(name); +} + +QString InstanceManager::instancePath(const QString& instanceName) const +{ + return QDir::fromNativeSeparators(instancesPath() + "/" + instanceName); +} -QString InstanceManager::instancePath() const +QString InstanceManager::instancesPath() const { return QDir::fromNativeSeparators( QStandardPaths::writableLocation(QStandardPaths::DataLocation)); } +QString InstanceManager::iniPath(const QDir& instanceDir) +{ + return instanceDir.filePath(QString::fromStdWString(AppConfig::iniFileName())); +} -QStringList InstanceManager::instances() const +std::vector<QDir> InstanceManager::instancePaths() const { const std::set<QString> ignore = { "cache", "qtwebengine", }; - const auto dirs = QDir(instancePath()) - .entryList(QDir::Dirs | QDir::NoDotAndDotDot); + const QDir root(instancesPath()); + const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - QStringList list; + std::vector<QDir> list; for (auto&& d : dirs) { if (!ignore.contains(QFileInfo(d).fileName().toLower())) { - list.push_back(d); + list.push_back(root.filePath(d)); } } 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) { - return (dataPath == qApp->applicationDirPath()); + return (dataPath == portablePath()); +} + +QString InstanceManager::portablePath() +{ + return qApp->applicationDirPath(); } -bool InstanceManager::portableInstall() const +bool InstanceManager::portableInstanceExists() const { return QFile::exists(qApp->applicationDirPath() + "/" + QString::fromStdWString(AppConfig::iniFileName())); @@ -296,67 +419,89 @@ bool InstanceManager::portableInstallIsLocked() const QString::fromStdWString(AppConfig::portableLockFileName())); } - bool InstanceManager::allowedToChangeInstance() const { return !portableInstallIsLocked(); } - -void InstanceManager::createDataPath(const QString &dataPath) const +const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( + const QDir& instanceDir, const PluginContainer& plugins) const { - if (!QDir(dataPath).exists()) { - if (!QDir().mkpath(dataPath)) { - throw MOBase::MyException( - QObject::tr("failed to create %1").arg(dataPath)); - } else { - QMessageBox::information( - nullptr, QObject::tr("Data directory created"), - QObject::tr("New data directory created at %1. If you don't want to " - "store a lot of data there, reconfigure the storage " - "directories via settings.").arg(dataPath)); - } + const QString ini = iniPath(instanceDir); + + Settings s(ini); + + if (s.iniStatus() != QSettings::NoError) + { + log::error("failed to load settings from {}", ini); + return nullptr; } -} + const auto instanceGameName = s.game().name(); -QString InstanceManager::determineDataPath() -{ - QString instanceId = currentInstance(); - if (portableInstallIsLocked()) + if (instanceGameName && !instanceGameName->isEmpty()) { - instanceId.clear(); + for (const IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (instanceGameName->compare(game->gameName(), Qt::CaseInsensitive) == 0) { + return game; + } + } + + log::error( + "no plugin reports game name '{}' found in ini {}", + *instanceGameName, ini); } - if (instanceId.isEmpty() && !m_Reset && (m_overrideInstance || portableInstall())) + else { - // startup, apparently using portable mode before - return qApp->applicationDirPath(); + log::error("no game name found in ini {}", ini); } - QString dataPath = QDir::fromNativeSeparators( - QStandardPaths::writableLocation(QStandardPaths::DataLocation) - + "/" + instanceId); + log::error("falling back on looksValid check"); - if (!m_overrideInstance && (instanceId.isEmpty() || !QFileInfo::exists(dataPath))) { - instanceId = chooseInstance(instances()); - setCurrentInstance(instanceId); - if (!instanceId.isEmpty()) { - dataPath = QDir::fromNativeSeparators( - QStandardPaths::writableLocation(QStandardPaths::DataLocation) - + "/" + instanceId); + const auto gameDir = s.game().directory(); + + if (gameDir && !gameDir->isEmpty()) + { + for (const IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (game->looksValid(*gameDir)) { + return game; + } } + + log::error( + "no plugins appear to support game directory '{}' from ini {}", + *gameDir, ini); + } + else + { + log::error("no game directory found in ini {}", ini); } - if (instanceId.isEmpty()) { - return qApp->applicationDirPath(); - } else { - createDataPath(dataPath); + return nullptr; +} + +QString InstanceManager::makeUniqueName(const QString& instanceName) const +{ + const QString sanitized = sanitizeInstanceName(instanceName); + + QString name = sanitized; + for (int i=2; i<100; ++i) { + if (!instanceExists(name)) { + return name; + } - return dataPath; + name = QString("%1 (%2)").arg(sanitized).arg(i); } + + return {}; } +bool InstanceManager::instanceExists(const QString& instanceName) const +{ + const QDir root = instancesPath(); + return root.exists(instanceName); +} QString InstanceManager::sanitizeInstanceName(const QString &name) const { @@ -375,3 +520,12 @@ QString InstanceManager::sanitizeInstanceName(const QString &name) const } return new_name; } + +bool InstanceManager::validInstanceName(const QString& instanceName) const +{ + if (instanceName.isEmpty()) { + return false; + } + + return (instanceName == sanitizeInstanceName(instanceName)); +} |
