/*
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 "createinstancedialog.h"
#include "createinstancedialogpages.h"
#include "filesystemutilities.h"
#include "instancemanagerdialog.h"
#include "nexusinterface.h"
#include "plugincontainer.h"
#include "selectiondialog.h"
#include "settings.h"
#include "shared/appconfig.h"
#include "shared/util.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace MOBase;
Instance::Instance(QString dir, bool portable, QString profileName)
: m_dir(std::move(dir)), m_portable(portable), m_plugin(nullptr),
m_profile(std::move(profileName))
{}
QString Instance::displayName() const
{
if (isPortable())
return QObject::tr("Portable");
else {
QDir instanceDir(m_dir.toUtf8());
return instanceDir.dirName();
}
}
QString Instance::gameName() const
{
if (!m_iniValuesRead && m_gameName.isEmpty()) {
std::scoped_lock lock(m_iniValuesMutex);
if (!m_iniValuesRead) {
readFromIni();
}
}
return m_gameName;
}
QString Instance::gameDirectory() const
{
if (!m_iniValuesRead && m_gameDir.isEmpty()) {
std::scoped_lock lock(m_iniValuesMutex);
if (!m_iniValuesRead) {
readFromIni();
}
}
return m_gameDir;
}
QString Instance::directory() const
{
return m_dir;
}
QString Instance::baseDirectory() const
{
if (!m_iniValuesRead) {
std::scoped_lock lock(m_iniValuesMutex);
if (!m_iniValuesRead) {
readFromIni();
}
}
return m_baseDir;
}
MOBase::IPluginGame* Instance::gamePlugin() const
{
return m_plugin;
}
QString Instance::profileName() const
{
if (!m_iniValuesRead) {
std::scoped_lock lock(m_iniValuesMutex);
if (!m_iniValuesRead) {
readFromIni();
}
}
return m_profile;
}
QString Instance::iniPath() const
{
return InstanceManager::singleton().iniPath(m_dir);
}
bool Instance::isPortable() const
{
return m_portable;
}
bool Instance::isActive() const
{
auto& m = InstanceManager::singleton();
if (auto i = m.currentInstance()) {
if (m_portable) {
return i->isPortable();
} else {
return (i->displayName() == displayName());
}
}
return false;
}
bool Instance::readFromIni() const
{
Settings s(iniPath());
if (s.iniStatus() != QSettings::NoError) {
log::error("can't read ini {}", iniPath());
m_iniValuesRead = true;
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;
}
if (m_gameDir.isEmpty()) {
if (auto v = s.game().directory())
m_gameDir = *v;
}
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);
m_iniValuesRead = true;
return true;
}
Instance::SetupResults Instance::setup(PluginContainer& plugins)
{
if (!m_iniValuesRead) {
std::scoped_lock lock(m_iniValuesMutex);
// read initial values from the ini
if (!m_iniValuesRead && !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);
}
// 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()) {
// don't write a variant to the ini if the plugin doesn't require one
s.game().setEdition(m_gameVariant);
}
}
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::Okay;
}
}
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::Okay;
}
}
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::Okay;
} 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) const
{
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);
}
// returns a list of files and folders that must be deleted when deleting
// this instance
//
std::vector 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