/* Copyright (C) 2016 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "instancemanager.h" #include "selectiondialog.h" #include "settings.h" #include "shared/appconfig.h" #include "plugincontainer.h" #include "shared/util.h" #include #include #include #include #include #include #include #include #include #include using namespace MOBase; 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(); } QString Instance::gameName() const { return m_gameName; } QString Instance::gameDirectory() const { return m_gameDir; } QDir Instance::directory() const { return m_dir; } MOBase::IPluginGame* Instance::gamePlugin() const { return m_plugin; } QString Instance::profileName() const { return m_profile; } QString Instance::iniPath() const { return InstanceManager::iniPath(m_dir); } bool Instance::isPortable() const { return m_portable; } Instance::SetupResults Instance::setup(PluginContainer& plugins) { Settings s(iniPath()); if (s.iniStatus() != QSettings::NoError) { log::error("can't read ini {}", iniPath()); return SetupResults::BadIni; } if (m_gameName.isEmpty()) { if (auto v=s.game().name()) m_gameName = *v; } 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; } } 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; } void Instance::setGame(const QString& name, const QString& dir) { m_gameName = name; m_gameDir = dir; } void Instance::setVariant(const QString& name) { m_gameVariant = name; } Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) { if (!m_gameName.isEmpty() && !m_gameDir.isEmpty()) { // normal case: both the name and dir are in the ini // find the plugin by name for (IPluginGame* game : plugins.plugins()) { if (m_gameName.compare(game->gameName(), Qt::CaseInsensitive) == 0) { // plugin found, check if the game directory is valid 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; } } 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()) { 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; } } 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()) { 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; } } void Instance::getProfile(const Settings& s) { if (!m_profile.isEmpty()) { // there's already a profile set up, probably an override return; } if (auto name=s.game().selectedProfileName()) { // use last profile m_profile = *name; return; } // profile missing from ini, use the default m_profile = QString::fromStdWString(AppConfig::defaultProfileName()); log::warn( "no profile found in ini {}, using default '{}'", iniPath(), m_profile); } InstanceManager::InstanceManager() { GlobalSettings::updateRegistryKey(); } InstanceManager &InstanceManager::instance() { static InstanceManager s_Instance; return s_Instance; } 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 InstanceManager::currentInstance() const { const QString profile = m_overrideProfile ? m_overrideProfileName : ""; if (portableInstallIsLocked()) { // force portable instance return Instance(QDir(portablePath()), true, profile); } QString name; if (m_overrideInstance) name = m_overrideInstanceName; else name = GlobalSettings::currentInstance(); 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::instancesPath() const { return QDir::fromNativeSeparators( QStandardPaths::writableLocation(QStandardPaths::DataLocation)); } QString InstanceManager::iniPath(const QDir& instanceDir) { return instanceDir.filePath(QString::fromStdWString(AppConfig::iniFileName())); } std::vector InstanceManager::instancePaths() const { const std::set ignore = { "cache", "qtwebengine", }; const QDir root(instancesPath()); const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot); std::vector list; for (auto&& d : dirs) { if (!ignore.contains(QFileInfo(d).fileName().toLower())) { 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 == portablePath()); } QString InstanceManager::portablePath() { return qApp->applicationDirPath(); } bool InstanceManager::portableInstanceExists() const { return QFile::exists(qApp->applicationDirPath() + "/" + QString::fromStdWString(AppConfig::iniFileName())); } bool InstanceManager::portableInstallIsLocked() const { return QFile::exists(qApp->applicationDirPath() + "/" + QString::fromStdWString(AppConfig::portableLockFileName())); } bool InstanceManager::allowedToChangeInstance() const { return !portableInstallIsLocked(); } const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( const QDir& instanceDir, const PluginContainer& plugins) const { 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(); if (instanceGameName && !instanceGameName->isEmpty()) { for (const IPluginGame* game : plugins.plugins()) { if (instanceGameName->compare(game->gameName(), Qt::CaseInsensitive) == 0) { return game; } } log::error( "no plugin reports game name '{}' found in ini {}", *instanceGameName, ini); } else { log::error("no game name found in ini {}", ini); } log::error("falling back on looksValid check"); const auto gameDir = s.game().directory(); if (gameDir && !gameDir->isEmpty()) { for (const IPluginGame* game : plugins.plugins()) { 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); } 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; } 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 { QString new_name = name; // Restrict the allowed characters new_name = new_name.remove(QRegExp("[^A-Za-z0-9 _=+;!@#$%^'\\-\\.\\[\\]\\{\\}\\(\\)]")); // Don't end in spaces and periods new_name = new_name.remove(QRegExp("\\.*$")); new_name = new_name.remove(QRegExp(" *$")); // Recurse until stuff stops changing if (new_name != name) { return sanitizeInstanceName(new_name); } return new_name; } bool InstanceManager::validInstanceName(const QString& instanceName) const { if (instanceName.isEmpty()) { return false; } return (instanceName == sanitizeInstanceName(instanceName)); }