/*
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 .
*/
#include
#include "installationmanager.h"
#include "categories.h"
#include "filesystemutilities.h"
#include "iplugininstallercustom.h"
#include "iplugininstallersimple.h"
#include "messagedialog.h"
#include "modinfo.h"
#include "nexusinterface.h"
#include "queryoverwritedialog.h"
#include "questionboxmemory.h"
#include "report.h"
#include "selectiondialog.h"
#include "settings.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "archivefiletree.h"
using namespace MOBase;
using namespace MOShared;
InstallationResult::InstallationResult(IPluginInstaller::EInstallResult result)
: m_result(result), m_name(), m_iniTweaks(false), m_backup(false), m_merged(false),
m_replaced(false)
{}
template
static T resolveFunction(QLibrary& lib, const char* name)
{
T temp = reinterpret_cast(lib.resolve(name));
if (temp == nullptr) {
throw std::runtime_error(QObject::tr("invalid 7-zip32.dll: %1")
.arg(lib.errorString())
.toLatin1()
.constData());
}
return temp;
}
InstallationManager::InstallationManager() : m_ParentWidget(nullptr), m_IsRunning(false)
{
m_ArchiveHandler = CreateArchive();
if (!m_ArchiveHandler->isValid()) {
throw MyException(getErrorString(m_ArchiveHandler->getLastError()));
}
m_ArchiveHandler->setLogCallback([](auto level, auto const& message) {
using LogLevel = Archive::LogLevel;
switch (level) {
case LogLevel::Debug:
log::debug("{}", message);
break;
case LogLevel::Info:
log::info("{}", message);
break;
case LogLevel::Warning:
log::warn("{}", message);
break;
case LogLevel::Error:
log::error("{}", message);
break;
}
});
// Connect the query password slot - This is the only way I found to be able to query
// user from a separate thread. We use a BlockingQueuedConnection so that calling
// passwordRequested() will block until the end of the slot.
connect(this, &InstallationManager::passwordRequested, this,
&InstallationManager::queryPassword, Qt::BlockingQueuedConnection);
}
InstallationManager::~InstallationManager() {}
void InstallationManager::setParentWidget(QWidget* widget)
{
m_ParentWidget = widget;
}
void InstallationManager::setPluginContainer(const PluginContainer* pluginContainer)
{
m_PluginContainer = pluginContainer;
}
void InstallationManager::queryPassword()
{
m_Password = QInputDialog::getText(m_ParentWidget, tr("Password required"),
tr("Password"), QLineEdit::Password);
}
bool InstallationManager::extractFiles(QString extractPath, QString title,
bool showFilenames, bool silent)
{
TimeThis tt("InstallationManager::extractFiles");
// Callback for errors:
QString errorMessage;
auto errorCallback = [&errorMessage, this](std::wstring const& message) {
m_ArchiveHandler->cancel();
errorMessage = QString::fromStdWString(message);
};
// The future that will hold the result:
QFuture future;
if (silent) {
future = QtConcurrent::run([&]() -> bool {
return m_ArchiveHandler->extract(extractPath.toStdWString(), nullptr, nullptr,
errorCallback);
});
future.waitForFinished();
} else {
QProgressDialog* installationProgress = new QProgressDialog(m_ParentWidget);
ON_BLOCK_EXIT([=]() {
installationProgress->cancel();
installationProgress->hide();
installationProgress->deleteLater();
});
installationProgress->setWindowFlags(installationProgress->windowFlags() &
(~Qt::WindowContextHelpButtonHint));
if (!title.isEmpty()) {
installationProgress->setWindowTitle(title);
}
installationProgress->setWindowModality(Qt::WindowModal);
installationProgress->setFixedSize(600, 100);
// Turn off auto-reset otherwize the progress dialog is reset before the end. This
// is kind of annoying because updateProgress consider percentage of progression
// through the archive (pack), while we are waiting for extracting archive entries,
// so the percentage of in updateProgress is not really related to the percentage of
// files extracted...
installationProgress->setAutoReset(false);
// Note: Using a loop with a progressUpdate() that only wake-up the loop. The
// event-loop will be used in a loop and not via exec() because connecting to
// QProgressDialog::setValue and using .exec() creates huge recursion that leads to
// stack-overflow. See https://bugreports.qt.io/browse/QTBUG-10561
QEventLoop loop;
connect(this, &InstallationManager::progressUpdate, &loop, &QEventLoop::wakeUp,
Qt::QueuedConnection);
// Cancelling progress only cancel the extraction, we do not force exiting the
// event-loop:
connect(installationProgress, &QProgressDialog::canceled, [this]() {
m_ArchiveHandler->cancel();
});
std::mutex mutex;
int currentProgress = 0;
QString currentFileName;
// The callbacks:
auto progressCallback = [this, ¤tProgress, &mutex](
auto progressType, uint64_t current, uint64_t total) {
if (progressType == Archive::ProgressType::EXTRACTION) {
{
std::scoped_lock guard(mutex);
currentProgress = static_cast(100 * current / total);
}
emit progressUpdate();
}
};
Archive::FileChangeCallback fileChangeCallback =
[this, ¤tFileName, &mutex](auto changeType, std::wstring const& file) {
if (changeType == Archive::FileChangeType::EXTRACTION_START) {
{
std::scoped_lock guard(mutex);
currentFileName = QString::fromStdWString(file);
}
emit progressUpdate();
}
};
// unpack only the files we need for the installer
QFutureWatcher futureWatcher;
connect(&futureWatcher, &QFutureWatcher::finished, &loop, &QEventLoop::wakeUp,
Qt::QueuedConnection);
futureWatcher.setFuture(QtConcurrent::run([&]() -> bool {
return m_ArchiveHandler->extract(extractPath.toStdWString(), progressCallback,
showFilenames ? fileChangeCallback : nullptr,
errorCallback);
}));
installationProgress->setModal(true);
installationProgress->show();
while (!futureWatcher.isFinished()) {
loop.processEvents(QEventLoop::AllEvents | QEventLoop::WaitForMoreEvents);
std::scoped_lock guard(mutex);
if (currentProgress != installationProgress->value()) {
installationProgress->setValue(currentProgress);
}
if (currentFileName != installationProgress->labelText()) {
installationProgress->setLabelText(currentFileName);
}
}
installationProgress->hide();
future = futureWatcher.future();
}
// Check the result:
if (!future.result()) {
if (m_ArchiveHandler->getLastError() == Archive::Error::ERROR_EXTRACT_CANCELLED) {
if (!errorMessage.isEmpty()) {
throw MyException(tr("Extraction failed: %1").arg(errorMessage));
} else {
return false;
}
} else {
throw MyException(tr("Extraction failed: %1")
.arg(static_cast(m_ArchiveHandler->getLastError())));
}
}
return true;
}
QString InstallationManager::extractFile(std::shared_ptr entry,
bool silent)
{
QStringList result = this->extractFiles({entry}, silent);
return result.isEmpty() ? QString() : result[0];
}
QStringList InstallationManager::extractFiles(
std::vector> const& entries, bool silent)
{
// Remove the directory since mapToArchive would add them:
std::vector> files;
std::copy_if(entries.begin(), entries.end(), std::back_inserter(files),
[](auto const& entry) {
return entry->isFile();
});
// Update the archive:
ArchiveFileTree::mapToArchive(*m_ArchiveHandler, files);
// Retrieve the file path:
QStringList result;
for (auto& entry : files) {
auto path = entry->path();
result.append(QDir::tempPath().append("/").append(path));
m_TempFilesToDelete.insert(path);
}
if (!extractFiles(QDir::tempPath(), tr("Extracting files"), false, silent)) {
return QStringList();
}
return result;
}
QString
InstallationManager::createFile(std::shared_ptr entry)
{
// Use QTemporaryFile to create the temporary file with the given template:
QTemporaryFile tempFile(
QDir::cleanPath(QDir::tempPath() + QDir::separator() + "mo2-install"));
// Turn-off autoRemove otherwise the file is deleted when destructor is called:
tempFile.setAutoRemove(false);
// Open/Close the file so that installer can use it properly:
if (!tempFile.open()) {
return QString();
}
tempFile.close();
// fileName() returns the full path since we provide a full path in the constructor:
const QString absPath = tempFile.fileName();
m_CreatedFiles[entry] = absPath;
m_TempFilesToDelete.insert(QDir::temp().relativeFilePath(absPath));
// Returns the path with native separators:
return QDir::toNativeSeparators(absPath);
}
void InstallationManager::cleanCreatedFiles(
std::shared_ptr fileTree)
{
// We simply have to check if all the entries have fileTree as a parent:
for (auto it = std::begin(m_CreatedFiles); it != std::end(m_CreatedFiles);) {
// Find the parent - Could this be in FileTreeEntry?
bool found = false;
{
auto parent = it->first->parent();
while (parent && !found) {
if (parent == fileTree) {
found = true;
} else {
parent = parent->parent();
}
}
}
// If the parent was not found, we remove the entry, otherwize we move to the next
// one:
if (!found) {
it = m_CreatedFiles.erase(it);
} else {
++it;
}
}
}
IPluginInstaller::EInstallResult
InstallationManager::installArchive(GuessedValue& modName,
const QString& archiveName, int modId)
{
// in earlier versions the modName was copied here and the copy passed to install. I
// don't know why I did this and it causes a problem if this is called by the bundle
// installer and the bundled installer adds additional names that then end up being
// used, because the caller will then not have the right name.
return install(archiveName, modName, modId).result();
}
QString InstallationManager::generateBackupName(const QString& directoryName) const
{
QString backupName = directoryName + "_backup";
if (QDir(backupName).exists()) {
int idx = 2;
QString temp = backupName + QString::number(idx);
while (QDir(temp).exists()) {
++idx;
temp = backupName + QString::number(idx);
}
backupName = temp;
}
return backupName;
}
InstallationResult InstallationManager::testOverwrite(GuessedValue& modName)
{
QString targetDirectory =
QDir::fromNativeSeparators(m_ModsDirectory + "\\" + modName);
// this is only returned on success
InstallationResult result{IPluginInstaller::RESULT_SUCCESS};
while (QDir(targetDirectory).exists()) {
Settings& settings(Settings::instance());
const bool backup = settings.keepBackupOnInstall();
QueryOverwriteDialog overwriteDialog(m_ParentWidget,
backup ? QueryOverwriteDialog::BACKUP_YES
: QueryOverwriteDialog::BACKUP_NO);
if (overwriteDialog.exec()) {
settings.setKeepBackupOnInstall(overwriteDialog.backup());
if (overwriteDialog.backup()) {
QString backupDirectory = generateBackupName(targetDirectory);
if (!copyDir(targetDirectory, backupDirectory, false)) {
reportError(tr("Failed to create backup"));
return {IPluginInstaller::RESULT_FAILED};
}
}
result.m_merged = overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE;
result.m_replaced = overwriteDialog.action() == QueryOverwriteDialog::ACT_REPLACE;
result.m_backup = overwriteDialog.backup();
if (overwriteDialog.action() == QueryOverwriteDialog::ACT_RENAME) {
bool ok = false;
QString name = QInputDialog::getText(m_ParentWidget, tr("Mod Name"), tr("Name"),
QLineEdit::Normal, modName, &ok);
if (ok && !name.isEmpty()) {
modName.update(name, GUESS_USER);
if (!ensureValidModName(modName)) {
return {IPluginInstaller::RESULT_FAILED};
}
targetDirectory = QDir::fromNativeSeparators(m_ModsDirectory) + "/" + modName;
}
} else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_REPLACE) {
unsigned int idx = ModInfo::getIndex(modName);
if (idx != UINT_MAX) {
auto modInfo = ModInfo::getByIndex(idx);
// mark the old install file as uninstalled
emit modReplaced(modInfo->installationFile());
}
// save original settings like categories. Because it makes sense
QString metaFilename = targetDirectory + "/meta.ini";
QFile settingsFile(metaFilename);
QByteArray originalSettings;
if (settingsFile.open(QIODevice::ReadOnly)) {
originalSettings = settingsFile.readAll();
settingsFile.close();
}
// remove the directory with all content, then recreate it empty
shellDelete(QStringList(targetDirectory));
if (!QDir().mkdir(targetDirectory)) {
// windows may keep the directory around for a moment, preventing its
// re-creation. Not sure if this still happens with shellDelete
Sleep(100);
QDir().mkdir(targetDirectory);
}
// restore the saved settings
if (settingsFile.open(QIODevice::WriteOnly)) {
settingsFile.write(originalSettings);
settingsFile.close();
} else {
log::error("failed to restore original settings: {}", metaFilename);
}
return result;
} else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE) {
return result;
} else /* if (overwriteDialog.action() == QueryOverwriteDialog::ACT_NONE) */
{
return {IPluginInstaller::RESULT_CANCELED};
}
} else {
return {IPluginInstaller::RESULT_CANCELED};
}
}
QDir().mkdir(targetDirectory);
return result;
}
bool InstallationManager::ensureValidModName(GuessedValue& name) const
{
while (name->isEmpty()) {
bool ok;
name.update(
QInputDialog::getText(
m_ParentWidget, tr("Invalid name"),
tr("The name you entered is invalid, please enter a different one."),
QLineEdit::Normal, "", &ok),
GUESS_USER);
if (!ok) {
return false;
}
}
return true;
}
InstallationResult InstallationManager::doInstall(ModInstallationInfo& info)
{
if (!ensureValidModName(info.modName)) {
return {IPluginInstaller::RESULT_FAILED};
}
// determine target directory
InstallationResult result = testOverwrite(info.modName);
if (!result) {
return result;
}
const bool merge = result.merged();
result.m_name = info.modName;
QString targetDirectory = QDir(m_ModsDirectory + "/" + info.modName).canonicalPath();
QString targetDirectoryNative = QDir::toNativeSeparators(targetDirectory);
log::debug("installing to \"{}\"", targetDirectoryNative);
if (!extractFiles(targetDirectory, "", true, false)) {
return {IPluginInstaller::RESULT_CANCELED};
}
// Copy the created files:
for (auto& p : m_CreatedFiles) {
QString destPath =
QDir::cleanPath(targetDirectory + QDir::separator() + p.first->path());
log::debug("Moving {} to {}.", p.second, destPath);
// We need to remove the path if it exists:
if (QFile::exists(destPath)) {
QFile::remove(destPath);
}
QDir dir = QFileInfo(destPath).absoluteDir();
if (!dir.exists()) {
dir.mkpath(".");
}
QFile::copy(p.second, destPath);
}
QSettings settingsFile(targetDirectory + "/meta.ini", QSettings::IniFormat);
// overwrite settings only if they are actually are available or haven't been set
// before
if ((info.gameName != "") || !settingsFile.contains("gameName")) {
settingsFile.setValue("gameName", info.gameName);
}
if ((info.modID != 0) || !settingsFile.contains("modid")) {
settingsFile.setValue("modid", info.modID);
}
if (!settingsFile.contains("version") ||
(!info.version.isEmpty() &&
(!merge || (VersionInfo(info.version) >=
VersionInfo(settingsFile.value("version").toString()))))) {
settingsFile.setValue("version", info.version);
}
if (!info.newestVersion.isEmpty() || !settingsFile.contains("newestVersion")) {
settingsFile.setValue("newestVersion", info.newestVersion);
}
// issue #51 used to overwrite the manually set categories
if (!settingsFile.contains("category")) {
settingsFile.setValue("category", QString::number(info.categoryID));
}
settingsFile.setValue("nexusFileStatus", info.fileCategoryID);
settingsFile.setValue("installationFile", m_CurrentFile);
settingsFile.setValue("repository", info.repository);
settingsFile.setValue("author", info.author);
settingsFile.setValue("uploader", info.uploader);
settingsFile.setValue("uploaderUrl", info.uploaderUrl);
if (!merge) {
// this does not clear the list we have in memory but the mod is going to have to be
// re-read anyway btw.: installedFiles were written with beginWriteArray but we can
// still clear it with beginGroup. nice
settingsFile.beginGroup("installedFiles");
settingsFile.remove("");
settingsFile.endGroup();
}
return result;
}
bool InstallationManager::wasCancelled() const
{
return m_ArchiveHandler->getLastError() == Archive::Error::ERROR_EXTRACT_CANCELLED;
}
bool InstallationManager::isRunning() const
{
return m_IsRunning;
}
void InstallationManager::postInstallCleanup()
{
// Clear the list of created files:
m_CreatedFiles.clear();
// Close the archive:
m_ArchiveHandler->close();
// directories we may want to remove. sorted from longest to shortest to ensure we
// remove subdirectories first.
auto longestFirst = [](const QString& LHS, const QString& RHS) -> bool {
if (LHS.size() != RHS.size())
return LHS.size() > RHS.size();
else
return LHS < RHS;
};
std::set>
directoriesToRemove(longestFirst);
// clean up temp files
// TODO: this doesn't yet remove directories. Also, the files may be left there if
// this point isn't reached
for (const QString& tempFile : m_TempFilesToDelete) {
QFileInfo fileInfo(QDir::tempPath() + "/" + tempFile);
if (fileInfo.exists()) {
if (!fileInfo.isReadable() || !fileInfo.isWritable()) {
QFile::setPermissions(fileInfo.absoluteFilePath(),
QFile::ReadOther | QFile::WriteOther);
}
if (!QFile::remove(fileInfo.absoluteFilePath())) {
log::warn("Unable to delete {}", fileInfo.absoluteFilePath());
}
}
directoriesToRemove.insert(fileInfo.absolutePath());
}
m_TempFilesToDelete.clear();
// try to delete each directory we had temporary files in. the call fails for
// non-empty directories which is ok
for (const QString& dir : directoriesToRemove) {
QDir().rmdir(dir);
}
}
InstallationResult InstallationManager::install(const QString& fileName,
GuessedValue& modName,
int modID)
{
m_IsRunning = true;
ON_BLOCK_EXIT([this]() {
m_IsRunning = false;
});
QFileInfo fileInfo(fileName);
if (!getSupportedExtensions().contains(fileInfo.suffix(), Qt::CaseInsensitive)) {
reportError(tr("File format \"%1\" not supported").arg(fileInfo.suffix()));
return InstallationResult(IPluginInstaller::RESULT_FAILED);
}
modName.setFilter(&fixDirectoryName);
modName.update(QFileInfo(fileName).completeBaseName(), GUESS_FALLBACK);
// read out meta information from the download if available
QString gameName = "";
QString version = "";
QString newestVersion = "";
int category = 0;
int categoryID = 0;
int fileCategoryID = 1;
QString repository = "Nexus";
QString author = "";
QString uploader = "";
QString uploaderUrl = "";
QString metaName = fileName + ".meta";
if (QFile(metaName).exists()) {
QSettings metaFile(metaName, QSettings::IniFormat);
gameName = metaFile.value("gameName", "").toString();
modID = metaFile.value("modID", 0).toInt();
QTextDocument doc;
doc.setHtml(metaFile.value("name", "").toString());
modName.update(doc.toPlainText(), GUESS_FALLBACK);
modName.update(metaFile.value("modName", "").toString(), GUESS_META);
version = metaFile.value("version", "").toString();
newestVersion = metaFile.value("newestVersion", "").toString();
category = metaFile.value("category", 0).toInt();
unsigned int categoryIndex = CategoryFactory::instance().resolveNexusID(category);
if (category != 0 && categoryIndex == 0U &&
Settings::instance().nexus().categoryMappings()) {
QMessageBox nexusQuery;
nexusQuery.setWindowTitle(tr("No category found"));
nexusQuery.setText(tr(
"This Nexus category has not yet been mapped. Do you wish to proceed without "
"setting a category, proceed and disable automatic Nexus mappings, or stop "
"and configure your category mappings?"));
QPushButton* proceedButton =
nexusQuery.addButton(tr("&Proceed"), QMessageBox::YesRole);
QPushButton* disableButton =
nexusQuery.addButton(tr("&Disable"), QMessageBox::AcceptRole);
QPushButton* stopButton =
nexusQuery.addButton(tr("&Stop && Configure"), QMessageBox::DestructiveRole);
nexusQuery.exec();
if (nexusQuery.clickedButton() == disableButton) {
Settings::instance().nexus().setCategoryMappings(false);
} else if (nexusQuery.clickedButton() == stopButton) {
return MOBase::IPluginInstaller::RESULT_CATEGORYREQUESTED;
}
} else {
categoryID = CategoryFactory::instance().getCategoryID(categoryIndex);
}
repository = metaFile.value("repository", "").toString();
fileCategoryID = metaFile.value("fileCategory", 1).toInt();
author = metaFile.value("author", "").toString();
uploader = metaFile.value("uploader", "").toString();
uploaderUrl = metaFile.value("uploaderUrl", "").toString();
}
if (version.isEmpty()) {
QDateTime lastMod = fileInfo.lastModified();
version = "d" + lastMod.toString("yyyy.M.d");
}
{ // guess the mod name and mod if from the file name if there was no meta
// information
QString guessedModName;
int guessedModID = modID;
NexusInterface::interpretNexusFileName(QFileInfo(fileName).fileName(),
guessedModName, guessedModID, false);
if ((modID == 0) && (guessedModID != -1)) {
modID = guessedModID;
} else if (modID != guessedModID) {
log::debug("passed mod id: {}, guessed id: {}", modID, guessedModID);
}
modName.update(guessedModName, GUESS_GOOD);
}
m_CurrentFile = fileInfo.absoluteFilePath();
if (fileInfo.dir() == QDir(m_DownloadsDirectory)) {
m_CurrentFile = fileInfo.fileName();
}
log::debug("using mod name \"{}\" (id {}) -> {}", QString(modName), modID,
m_CurrentFile);
// If there's an archive already open, close it. This happens with the bundle
// installer when it uncompresses a split archive, then finds it has a real archive
// to deal with.
m_ArchiveHandler->close();
// open the archive and construct the directory tree the installers work on
bool archiveOpen =
m_ArchiveHandler->open(fileName.toStdWString(), [this]() -> std::wstring {
m_Password = QString();
// Note: If we are not in the Qt event thread, we cannot use queryPassword()
// directly, so we emit passwordRequested() that is connected to
// queryPassword(). The connection is made using Qt::BlockingQueuedConnection,
// so the emit "call" is actually blocking. We cannot use emit if we are in the
// even thread, otherwize we have a deadlock.
if (QThread::currentThread() != QApplication::instance()->thread()) {
emit passwordRequested();
} else {
queryPassword();
}
return m_Password.toStdWString();
});
if (!archiveOpen) {
log::debug("integrated archiver can't open {}: {} ({})", fileName,
getErrorString(m_ArchiveHandler->getLastError()),
m_ArchiveHandler->getLastError());
}
ON_BLOCK_EXIT(std::bind(&InstallationManager::postInstallCleanup, this));
std::shared_ptr filesTree =
archiveOpen ? ArchiveFileTree::makeTree(*m_ArchiveHandler) : nullptr;
auto installers = m_PluginContainer->plugins();
std::sort(installers.begin(), installers.end(),
[](IPluginInstaller* lhs, IPluginInstaller* rhs) {
return lhs->priority() > rhs->priority();
});
InstallationResult installResult(IPluginInstaller::RESULT_NOTATTEMPTED);
for (IPluginInstaller* installer : installers) {
// don't use inactive installers (installer can't be null here but vc static code
// analysis thinks it could)
if ((installer == nullptr) || !m_PluginContainer->isEnabled(installer)) {
continue;
}
// try only manual installers if that was requested
if (installResult.result() == IPluginInstaller::RESULT_MANUALREQUESTED) {
if (!installer->isManualInstaller()) {
continue;
}
} else if (installResult.result() != IPluginInstaller::RESULT_NOTATTEMPTED) {
break;
}
try {
{ // simple case
IPluginInstallerSimple* installerSimple =
dynamic_cast(installer);
if ((installerSimple != nullptr) && (filesTree != nullptr) &&
(installer->isArchiveSupported(filesTree))) {
installResult.m_result =
installerSimple->install(modName, filesTree, version, modID);
if (installResult) {
// Downcast to an actual ArchiveFileTree and map to the archive. Test if
// the tree is still an ArchiveFileTree, otherwize it means the installer
// did some bad stuff.
ArchiveFileTree* p = dynamic_cast(filesTree.get());
if (p == nullptr) {
throw IncompatibilityException(
tr("Invalid file tree returned by plugin."));
}
// Detach the file tree (this ensure the parent is null and call to path()
// stops at this root):
p->detach();
p->mapToArchive(*m_ArchiveHandler);
// Clean the created files:
cleanCreatedFiles(filesTree);
// the simple installer only prepares the installation, the rest
// works the same for all installers
ModInstallationInfo info{
modName, gameName, modID, version, newestVersion, categoryID,
fileCategoryID, repository, author, uploader, uploaderUrl};
installResult = doInstall(info);
}
}
}
if (installResult.result() != IPluginInstaller::RESULT_CANCELED) { // custom case
IPluginInstallerCustom* installerCustom =
dynamic_cast(installer);
if ((installerCustom != nullptr) &&
(((filesTree != nullptr) && installer->isArchiveSupported(filesTree)) ||
((filesTree == nullptr) &&
installerCustom->isArchiveSupported(fileName)))) {
std::set installerExt = installerCustom->supportedExtensions();
if (installerExt.find(fileInfo.suffix()) != installerExt.end()) {
installResult.m_result =
installerCustom->install(modName, gameName, fileName, version, modID);
unsigned int idx = ModInfo::getIndex(modName);
if (idx != UINT_MAX) {
ModInfo::Ptr info = ModInfo::getByIndex(idx);
info->setRepository(repository);
}
}
}
}
} catch (const IncompatibilityException& e) {
log::error("plugin \"{}\" incompatible: {}", installer->name(), e.what());
}
// act upon the installation result. at this point the files have already been
// extracted to the correct location
switch (installResult.result()) {
case IPluginInstaller::RESULT_FAILED: {
QMessageBox::information(qApp->activeWindow(), tr("Installation failed"),
tr("Something went wrong while installing this mod."),
QMessageBox::Ok);
return installResult;
} break;
case IPluginInstaller::RESULT_SUCCESS:
case IPluginInstaller::RESULT_SUCCESSCANCEL: {
if (filesTree != nullptr) {
auto iniTweakEntry = filesTree->find("INI Tweaks", FileTreeEntry::DIRECTORY);
installResult.m_iniTweaks =
iniTweakEntry != nullptr && !iniTweakEntry->astree()->empty();
}
installResult.m_result = IPluginInstaller::RESULT_SUCCESS;
return installResult;
} break;
case IPluginInstaller::RESULT_NOTATTEMPTED:
case IPluginInstaller::RESULT_MANUALREQUESTED: {
continue;
}
default:
return installResult;
}
}
if (installResult.result() == IPluginInstaller::RESULT_NOTATTEMPTED) {
reportError(
tr("None of the available installer plugins were able to handle that archive.\n"
"This is likely due to a corrupted or incompatible download or unrecognized "
"archive format."));
}
return installResult;
}
QString InstallationManager::getErrorString(Archive::Error errorCode)
{
switch (errorCode) {
case Archive::Error::ERROR_NONE: {
return tr("no error");
} break;
case Archive::Error::ERROR_LIBRARY_NOT_FOUND: {
return tr("7z.dll not found");
} break;
case Archive::Error::ERROR_LIBRARY_INVALID: {
return tr("7z.dll isn't valid");
} break;
case Archive::Error::ERROR_ARCHIVE_NOT_FOUND: {
return tr("archive not found");
} break;
case Archive::Error::ERROR_FAILED_TO_OPEN_ARCHIVE: {
return tr("failed to open archive");
} break;
case Archive::Error::ERROR_INVALID_ARCHIVE_FORMAT: {
return tr("unsupported archive type");
} break;
case Archive::Error::ERROR_LIBRARY_ERROR: {
return tr("internal library error");
} break;
case Archive::Error::ERROR_ARCHIVE_INVALID: {
return tr("archive invalid");
} break;
default: {
// this probably means the archiver.dll is newer than this
return tr("unknown archive error");
} break;
}
}
QStringList InstallationManager::getSupportedExtensions() const
{
std::set supportedExtensions(
{"zip", "rar", "7z", "fomod", "001"});
for (auto* installer : m_PluginContainer->plugins()) {
if (m_PluginContainer->isEnabled(installer)) {
if (auto* installerCustom = dynamic_cast(installer)) {
std::set extensions = installerCustom->supportedExtensions();
supportedExtensions.insert(extensions.begin(), extensions.end());
}
}
}
return QStringList(supportedExtensions.begin(), supportedExtensions.end());
}
void InstallationManager::notifyInstallationStart(QString const& archive,
bool reinstallation,
ModInfo::Ptr currentMod)
{
auto& installers = m_PluginContainer->plugins();
for (auto* installer : installers) {
if (m_PluginContainer->isEnabled(installer)) {
installer->onInstallationStart(archive, reinstallation, currentMod.get());
}
}
}
void InstallationManager::notifyInstallationEnd(const InstallationResult& result,
ModInfo::Ptr newMod)
{
auto& installers = m_PluginContainer->plugins();
for (auto* installer : installers) {
if (m_PluginContainer->isEnabled(installer)) {
installer->onInstallationEnd(result.result(), newMod.get());
}
}
}