/* 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 "profile.h" #include "report.h" #include "gameinfo.h" #include "windows_error.h" #include "modinfo.h" #include "safewritefile.h" #include #include #include #include #include #include #include #include #include #include #include #include #define WIN32_LEAN_AND_MEAN #include #include #include using namespace MOBase; using namespace MOShared; Profile::Profile() : m_ModListWriter(std::bind(&Profile::writeModlistNow, this)) { } void Profile::touchFile(QString fileName) { QFile modList(m_Directory.filePath(fileName)); if (!modList.open(QIODevice::ReadWrite)) { throw std::runtime_error(QObject::tr("failed to create %1").arg(m_Directory.filePath(fileName)).toUtf8().constData()); } } Profile::Profile(const QString &name, IPluginGame *gamePlugin, bool useDefaultSettings) : m_ModListWriter(std::bind(&Profile::writeModlistNow, this)) , m_GamePlugin(gamePlugin) { QString profilesDir = qApp->property("dataPath").toString() + "/" + ToQString(AppConfig::profilesPath()); QDir profileBase(profilesDir); QString fixedName = name; if (!fixDirectoryName(fixedName)) { throw MyException(tr("invalid profile name %1").arg(name)); } if (!profileBase.exists() || !profileBase.mkdir(fixedName)) { throw MyException(tr("failed to create %1").arg(fixedName).toUtf8().constData()); } QString fullPath = profilesDir + "/" + fixedName; m_Directory = QDir(fullPath); try { // create files. Needs to happen after m_Directory was set! touchFile("modlist.txt"); touchFile("archives.txt"); IPluginGame::ProfileSettings settings = IPluginGame::CONFIGURATION | IPluginGame::MODS | IPluginGame::SAVEGAMES; if (useDefaultSettings) { settings |= IPluginGame::PREFER_DEFAULTS; } gamePlugin->initializeProfile(fullPath, settings); } catch (...) { // clean up in case of an error shellDelete(QStringList(profileBase.absoluteFilePath(fixedName))); throw; } refreshModStatus(); } Profile::Profile(const QDir &directory, IPluginGame *gamePlugin) : m_Directory(directory) , m_ModListWriter(std::bind(&Profile::writeModlistNow, this)) { assert(gamePlugin != nullptr); if (!QFile::exists(m_Directory.filePath("modlist.txt"))) { qWarning("missing modlist.txt in %s", qPrintable(directory.path())); touchFile(m_Directory.filePath("modlist.txt")); } IPluginGame::ProfileSettings settings = IPluginGame::CONFIGURATION | IPluginGame::MODS | IPluginGame::SAVEGAMES; gamePlugin->initializeProfile(directory, settings); if (!QFile::exists(getIniFileName())) { reportError(QObject::tr("\"%1\" is missing or inaccessible").arg(getIniFileName())); } refreshModStatus(); } Profile::Profile(const Profile &reference) : m_Directory(reference.m_Directory) , m_ModListWriter(std::bind(&Profile::writeModlistNow, this)) { refreshModStatus(); } Profile::~Profile() { m_ModListWriter.writeImmediately(true); } bool Profile::exists() const { return m_Directory.exists(); } void Profile::writeModlistNow() { if (!m_Directory.exists()) return; try { QString fileName = getModlistFileName(); SafeWriteFile file(fileName); file->write(QString("# This file was automatically generated by Mod Organizer.\r\n").toUtf8()); if (m_ModStatus.empty()) { return; } for (int i = m_ModStatus.size() - 1; i >= 0; --i) { // the priority order was inverted on load so it has to be inverted again unsigned int index = m_ModIndexByPriority[i]; if (index != UINT_MAX) { ModInfo::Ptr modInfo = ModInfo::getByIndex(index); std::vector flags = modInfo->getFlags(); if ((modInfo->getFixedPriority() == INT_MIN)) { if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) { file->write("*"); } else if (m_ModStatus[index].m_Enabled) { file->write("+"); } else { file->write("-"); } file->write(modInfo->name().toUtf8()); file->write("\r\n"); } } } if (file.commitIfDifferent(m_LastModlistHash)) { qDebug("%s saved", QDir::toNativeSeparators(fileName).toUtf8().constData()); } } catch (const std::exception &e) { reportError(tr("failed to write mod list: %1").arg(e.what())); return; } } void Profile::createTweakedIniFile() { QString tweakedIni = m_Directory.absoluteFilePath("initweaks.ini"); if (QFile::exists(tweakedIni) && !shellDeleteQuiet(tweakedIni)) { reportError(tr("failed to update tweaked ini file, wrong settings may be used: %1").arg(windowsErrorString(::GetLastError()))); return; } for (unsigned int i = 0; i < m_ModStatus.size(); ++i) { unsigned int idx = modIndexByPriority(i); if ((idx != UINT_MAX) && m_ModStatus[idx].m_Enabled) { ModInfo::Ptr modInfo = ModInfo::getByIndex(idx); mergeTweaks(modInfo, tweakedIni); } } mergeTweak(getProfileTweaks(), tweakedIni); bool error = false; if (!::WritePrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"1", ToWString(tweakedIni).c_str())) { error = true; } if (localSavesEnabled()) { if (!::WritePrivateProfileStringW(L"General", L"bUseMyGamesDirectory", L"0", ToWString(tweakedIni).c_str())) { error = true; } if (!::WritePrivateProfileStringW(L"General", L"SLocalSavePath", AppConfig::localSavePlaceholder(), ToWString(tweakedIni).c_str())) { error = true; } } if (error) { reportError(tr("failed to create tweaked ini: %1").arg(getCurrentErrorStringA().c_str())); } qDebug("%s saved", qPrintable(QDir::toNativeSeparators(tweakedIni))); } void Profile::refreshModStatus() { QFile file(getModlistFileName()); if (!file.open(QIODevice::ReadOnly)) { throw MyException(tr("\"%1\" is missing or inaccessible").arg(getModlistFileName())); } bool modStatusModified = false; m_ModStatus.clear(); m_ModStatus.resize(ModInfo::getNumMods()); std::set namesRead; // load mods from file and update enabled state and priority for them int index = 0; while (!file.atEnd()) { QByteArray line = file.readLine().trimmed(); bool enabled = true; QString modName; if (line.length() == 0) { // empty line continue; } else if (line.at(0) == '#') { // comment line continue; } else if (line.at(0) == '-') { enabled = false; modName = QString::fromUtf8(line.mid(1).trimmed().constData()); } else if ((line.at(0) == '+') || (line.at(0) == '*')) { modName = QString::fromUtf8(line.mid(1).trimmed().constData()); } else { modName = QString::fromUtf8(line.trimmed().constData()); } if (modName.size() > 0) { QString lookupName = modName; if (namesRead.find(lookupName) != namesRead.end()) { continue; } else { namesRead.insert(lookupName); } unsigned int modIndex = ModInfo::getIndex(lookupName); if (modIndex != UINT_MAX) { ModInfo::Ptr info = ModInfo::getByIndex(modIndex); if ((modIndex < m_ModStatus.size()) && (info->getFixedPriority() == INT_MIN)) { m_ModStatus[modIndex].m_Enabled = enabled || info->alwaysEnabled(); if (m_ModStatus[modIndex].m_Priority == -1) { if (static_cast(index) >= m_ModStatus.size()) { throw MyException(tr("invalid index %1").arg(index)); } m_ModStatus[modIndex].m_Priority = index++; } } else { qWarning("no mod state for \"%s\" (profile \"%s\")", qPrintable(modName), m_Directory.path().toUtf8().constData()); // need to rewrite the modlist to fix this modStatusModified = true; } } else { qDebug("mod \"%s\" (profile \"%s\") not found", qPrintable(modName), m_Directory.path().toUtf8().constData()); // need to rewrite the modlist to fix this modStatusModified = true; } } } int numKnownMods = index; int topInsert = 0; // invert priority order to match that of the pluginlist. Also // give priorities to mods not referenced in the profile for (size_t i = 0; i < m_ModStatus.size(); ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(i); if (modInfo->getFixedPriority() == INT_MAX) { continue; } if (m_ModStatus[i].m_Priority != -1) { m_ModStatus[i].m_Priority = numKnownMods - m_ModStatus[i].m_Priority - 1; } else { if (static_cast(index) >= m_ModStatus.size()) { throw MyException(tr("invalid index %1").arg(index)); } if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { m_ModStatus[i].m_Priority = --topInsert; } else { m_ModStatus[i].m_Priority = index++; } // also, mark the mod-list as changed modStatusModified = true; } } // to support insertion of new mods at the top we may now have mods with negative priority. shift them all up // to align priority with 0 if (topInsert < 0) { int offset = topInsert * -1; for (size_t i = 0; i < m_ModStatus.size(); ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(i); if (modInfo->getFixedPriority() == INT_MAX) { continue; } m_ModStatus[i].m_Priority += offset; } } file.close(); updateIndices(); if (modStatusModified) { m_ModListWriter.write(); } } void Profile::dumpModStatus() const { for (unsigned int i = 0; i < m_ModStatus.size(); ++i) { ModInfo::Ptr info = ModInfo::getByIndex(i); qWarning("%d: %s - %d (%s)", i, info->name().toUtf8().constData(), m_ModStatus[i].m_Priority, m_ModStatus[i].m_Enabled ? "enabled" : "disabled"); } } void Profile::updateIndices() { m_NumRegularMods = 0; m_ModIndexByPriority.clear(); m_ModIndexByPriority.resize(m_ModStatus.size(), UINT_MAX); for (unsigned int i = 0; i < m_ModStatus.size(); ++i) { int priority = m_ModStatus[i].m_Priority; if (priority < 0) { // don't assign this to mapping at all, it's probably the overwrite mod continue; } else if (priority >= static_cast(m_ModIndexByPriority.size())) { qCritical("invalid priority %d for mod", priority); continue; } else { ++m_NumRegularMods; m_ModIndexByPriority.at(priority) = i; } } } std::vector > Profile::getActiveMods() { std::vector > result; for (std::vector::const_iterator iter = m_ModIndexByPriority.begin(); iter != m_ModIndexByPriority.end(); ++iter) { if ((*iter != UINT_MAX) && m_ModStatus[*iter].m_Enabled) { ModInfo::Ptr modInfo = ModInfo::getByIndex(*iter); result.push_back(std::make_tuple(modInfo->internalName(), modInfo->absolutePath(), m_ModStatus[*iter].m_Priority)); } } unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { std::vector flags = mod->getFlags(); return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); if (overwriteIndex != UINT_MAX) { ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex); result.push_back(std::make_tuple(overwriteInfo->name(), overwriteInfo->absolutePath(), UINT_MAX)); } else { reportError(tr("Overwrite directory couldn't be parsed")); } return result; } unsigned int Profile::modIndexByPriority(unsigned int priority) const { if (priority >= m_ModStatus.size()) { throw MyException(tr("invalid priority %1").arg(priority)); } return m_ModIndexByPriority[priority]; } void Profile::setModEnabled(unsigned int index, bool enabled) { if (index >= m_ModStatus.size()) { throw MyException(tr("invalid index %1").arg(index)); } ModInfo::Ptr modInfo = ModInfo::getByIndex(index); // we could quit in the following case, this shouldn't be a change anyway, // but at least this allows the situation to be fixed in case of an error if (modInfo->alwaysEnabled()) { enabled = true; } if (enabled != m_ModStatus[index].m_Enabled) { m_ModStatus[index].m_Enabled = enabled; emit modStatusChanged(index); } } bool Profile::modEnabled(unsigned int index) const { if (index >= m_ModStatus.size()) { throw MyException(tr("invalid index %1").arg(index)); } return m_ModStatus[index].m_Enabled; } int Profile::getModPriority(unsigned int index) const { if (index >= m_ModStatus.size()) { throw MyException(tr("invalid index %1").arg(index)); } return m_ModStatus[index].m_Priority; } void Profile::setModPriority(unsigned int index, int &newPriority) { if (m_ModStatus.at(index).m_Overwrite) { // can't change priority of the overwrite return; } int newPriorityTemp = (std::max)(0, (std::min)(m_ModStatus.size() - 1, newPriority)); // don't try to place below overwrite while ((m_ModIndexByPriority.at(newPriorityTemp) >= m_ModStatus.size()) || m_ModStatus.at(m_ModIndexByPriority.at(newPriorityTemp)).m_Overwrite) { --newPriorityTemp; } int oldPriority = m_ModStatus.at(index).m_Priority; if (newPriorityTemp > oldPriority) { // priority is higher than the old, so the gap we left is in lower priorities for (int i = oldPriority + 1; i <= newPriorityTemp; ++i) { --m_ModStatus.at(m_ModIndexByPriority.at(i)).m_Priority; } } else { for (int i = newPriorityTemp; i < oldPriority; ++i) { ++m_ModStatus.at(m_ModIndexByPriority.at(i)).m_Priority; } ++newPriority; } m_ModStatus.at(index).m_Priority = newPriorityTemp; updateIndices(); m_ModListWriter.write(); } Profile *Profile::createPtrFrom(const QString &name, const Profile &reference, MOBase::IPluginGame *gamePlugin) { QString profileDirectory = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::profilesPath()) + "/" + name; reference.copyFilesTo(profileDirectory); return new Profile(QDir(profileDirectory), gamePlugin); } void Profile::copyFilesTo(QString &target) const { copyDir(m_Directory.absolutePath(), target, false); } std::vector Profile::splitDZString(const wchar_t *buffer) const { std::vector result; const wchar_t *pos = buffer; size_t length = wcslen(pos); while (length != 0U) { result.push_back(pos); pos += length + 1; length = wcslen(pos); } return result; } void Profile::mergeTweak(const QString &tweakName, const QString &tweakedIni) const { static const int bufferSize = 32768; std::wstring tweakNameW = ToWString(tweakName); std::wstring tweakedIniW = ToWString(tweakedIni); QScopedArrayPointer buffer(new wchar_t[bufferSize]); // retrieve a list of sections DWORD size = ::GetPrivateProfileSectionNamesW( buffer.data(), bufferSize, tweakNameW.c_str()); if (size == bufferSize - 2) { // unfortunately there is no good way to find the required size // of the buffer throw MyException(QString("Buffer too small. Please report this as a bug. " "For now you might want to split up %1").arg(tweakName)); } std::vector sections = splitDZString(buffer.data()); // now iterate over all sections and retrieve a list of keys in each for (std::vector::iterator iter = sections.begin(); iter != sections.end(); ++iter) { // retrieve the names of all keys size = ::GetPrivateProfileStringW(iter->c_str(), nullptr, nullptr, buffer.data(), bufferSize, tweakNameW.c_str()); if (size == bufferSize - 2) { throw MyException(QString("Buffer too small. Please report this as a bug. " "For now you might want to split up %1").arg(tweakName)); } std::vector keys = splitDZString(buffer.data()); for (std::vector::iterator keyIter = keys.begin(); keyIter != keys.end(); ++keyIter) { //TODO this treats everything as strings but how could I differentiate the type? ::GetPrivateProfileStringW(iter->c_str(), keyIter->c_str(), nullptr, buffer.data(), bufferSize, ToWString(tweakName).c_str()); ::WritePrivateProfileStringW(iter->c_str(), keyIter->c_str(), buffer.data(), tweakedIniW.c_str()); } } } void Profile::mergeTweaks(ModInfo::Ptr modInfo, const QString &tweakedIni) const { std::vector iniTweaks = modInfo->getIniTweaks(); for (std::vector::iterator iter = iniTweaks.begin(); iter != iniTweaks.end(); ++iter) { mergeTweak(*iter, tweakedIni); } } bool Profile::invalidationActive(bool *supported) const { IPluginGame *gamePlugin = qApp->property("managed_game").value(); BSAInvalidation *invalidation = gamePlugin->feature(); DataArchives *dataArchives = gamePlugin->feature(); if ((invalidation != nullptr) && (dataArchives != nullptr)) { if (supported != nullptr) { *supported = true; } for (const QString &archive : dataArchives->archives(this)) { if (invalidation->isInvalidationBSA(archive)) { return true; } } return false; } else { *supported = false; } return false; } void Profile::deactivateInvalidation() { IPluginGame *gamePlugin = qApp->property("managed_game").value(); BSAInvalidation *invalidation = gamePlugin->feature(); if (invalidation != nullptr) { invalidation->deactivate(this); } } void Profile::activateInvalidation() { IPluginGame *gamePlugin = qApp->property("managed_game").value(); BSAInvalidation *invalidation = gamePlugin->feature(); if (invalidation != nullptr) { invalidation->activate(this); } } bool Profile::localSavesEnabled() const { return m_Directory.exists("saves"); } bool Profile::enableLocalSaves(bool enable) { if (enable) { if (m_Directory.exists("_saves")) { m_Directory.rename("_saves", "saves"); } else { m_Directory.mkdir("saves"); } } else { QMessageBox::StandardButton res = QMessageBox::question(QApplication::activeModalWidget(), tr("Delete savegames?"), tr("Do you want to delete local savegames? (If you select \"No\", the save games " "will show up again if you re-enable local savegames)"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Cancel); if (res == QMessageBox::Yes) { shellDelete(QStringList(m_Directory.absoluteFilePath("saves"))); } else if (res == QMessageBox::No) { m_Directory.rename("saves", "_saves"); } else { return false; } } // default: assume success return true; } QString Profile::getModlistFileName() const { return QDir::cleanPath(m_Directory.absoluteFilePath("modlist.txt")); } QString Profile::getPluginsFileName() const { return QDir::cleanPath(m_Directory.absoluteFilePath("plugins.txt")); } QString Profile::getLoadOrderFileName() const { return QDir::cleanPath(m_Directory.absoluteFilePath("loadorder.txt")); } QString Profile::getLockedOrderFileName() const { return QDir::cleanPath(m_Directory.absoluteFilePath("lockedorder.txt")); } QString Profile::getArchivesFileName() const { return QDir::cleanPath(m_Directory.absoluteFilePath("archives.txt")); } QString Profile::getDeleterFileName() const { return QDir::cleanPath(m_Directory.absoluteFilePath("hide_plugins.txt")); } QString Profile::getIniFileName() const { std::wstring primaryIniFile = *(GameInfo::instance().getIniFileNames().begin()); return m_Directory.absoluteFilePath(ToQString(primaryIniFile)); } QString Profile::getProfileTweaks() const { return QDir::cleanPath(m_Directory.absoluteFilePath(ToQString(AppConfig::profileTweakIni()))); } QString Profile::absolutePath() const { return QDir::cleanPath(m_Directory.absolutePath()); } void Profile::rename(const QString &newName) { QDir profileDir(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::profilesPath())); profileDir.rename(name(), newName); m_Directory = profileDir.absoluteFilePath(newName); }