diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/profile.cpp | 1572 | ||||
| -rw-r--r-- | src/profile.h | 661 |
2 files changed, 1120 insertions, 1113 deletions
diff --git a/src/profile.cpp b/src/profile.cpp index ca47d327..77e4f813 100644 --- a/src/profile.cpp +++ b/src/profile.cpp @@ -1,783 +1,789 @@ -/*
-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 <http://www.gnu.org/licenses/>.
-*/
-
-#include "profile.h"
-#include "report.h"
-#include "gameinfo.h"
-#include "windows_error.h"
-#include "dummybsa.h"
-#include "modinfo.h"
-#include "safewritefile.h"
-#include <utility.h>
-#include <util.h>
-#include <error_report.h>
-#include <appconfig.h>
-#include <QMessageBox>
-#include <QApplication>
-#include <QSettings>
-#include <QTemporaryFile>
-
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <shlobj.h>
-#include <stdexcept>
-
-using namespace MOBase;
-using namespace MOShared;
-
-Profile::Profile()
- : m_SaveTimer(NULL)
-{
- initTimer();
-}
-
-Profile::Profile(const QString &name, bool useDefaultSettings)
- : m_SaveTimer(NULL)
-{
- initTimer();
- QString profilesDir = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir()));
- 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);
- QFile modList(m_Directory.filePath("modlist.txt"));
- if (!modList.open(QIODevice::ReadWrite)) {
- profileBase.rmdir(fixedName);
- throw std::runtime_error(QObject::tr("failed to create %1").arg(m_Directory.filePath("modlist.txt")).toUtf8().constData());
- }
- modList.close();
-
- try {
- GameInfo::instance().createProfile(ToWString(fullPath), useDefaultSettings);
- } catch (...) {
- // clean up in case of an error
- shellDelete(QStringList(profileBase.absoluteFilePath(fixedName)));
- throw;
- }
- refreshModStatus();
-}
-
-
-Profile::Profile(const QDir& directory)
- : m_Directory(directory), m_SaveTimer(NULL)
-{
- initTimer();
- if (!QFile::exists(m_Directory.filePath("modlist.txt"))) {
- qWarning("missing modlist.txt in %s", qPrintable(directory.path()));
- }
-
- GameInfo::instance().repairProfile(ToWString(m_Directory.absolutePath()));
-
- 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_SaveTimer(NULL)
-{
- initTimer();
- refreshModStatus();
-}
-
-
-Profile::~Profile()
-{
- writeModlistNow();
-}
-
-
-void Profile::initTimer()
-{
- m_SaveTimer = new QTimer(this);
- m_SaveTimer->setSingleShot(true);
- connect(m_SaveTimer, SIGNAL(timeout()), this, SLOT(writeModlistNow()));
-}
-
-
-bool Profile::exists() const
-{
- return m_Directory.exists();
-}
-
-
-void Profile::writeModlist() const
-{
- if (!m_SaveTimer->isActive()) {
- m_SaveTimer->start(2000);
- }
-}
-
-
-void Profile::cancelWriteModlist() const
-{
- m_SaveTimer->stop();
-}
-
-
-void Profile::writeModlistNow(bool onlyOnTimer) const
-{
- if (onlyOnTimer && !m_SaveTimer->isActive()) return;
-
- m_SaveTimer->stop();
- 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<ModInfo::EFlag> 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) {
- if (m_ModStatus[i].m_Enabled) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
- 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"1", 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.exists()) {
- throw MyException(tr("\"%1\" is missing or inaccessible").arg(getModlistFileName()));
- }
-
- bool modStatusModified = false;
- m_ModStatus.clear();
- m_ModStatus.resize(ModInfo::getNumMods());
-
- std::set<QString> namesRead;
-
- // load mods from file and update enabled state and priority for them
- file.open(QIODevice::ReadOnly);
- int index = 0;
- while (!file.atEnd()) {
- QByteArray line = file.readLine();
- 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 + (line.at(0) == '*' ? ModInfoForeign::INT_IDENTIFIER : "");
- 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<size_t>(index) >= m_ModStatus.size()) {
- throw MyException(tr("invalid index %1").arg(index));
- }
- m_ModStatus[modIndex].m_Priority = index++;
- }
- } else {
- qDebug("mod \"%s\" (profile \"%s\") not found",
- modName.toUtf8().constData(), m_Directory.path().toUtf8().constData());
- // need to rewrite the modlist to fix this
- modStatusModified = true;
- }
- }
- } else {
- // line was empty after trimming
- }
- }
-
- 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<size_t>(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) {
- writeModlist();
- }
-}
-
-
-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<int>(m_ModIndexByPriority.size())) {
- qCritical("invalid priority %d for mod", priority);
- continue;
- } else {
- ++m_NumRegularMods;
- m_ModIndexByPriority.at(priority) = i;
- }
- }
-}
-
-
-std::vector<std::tuple<QString, QString, int> > Profile::getActiveMods()
-{
- std::vector<std::tuple<QString, QString, int> > result;
- for (std::vector<unsigned int>::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<ModInfo::EFlag> 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<int>)(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();
- writeModlist();
-}
-
-
-Profile Profile::createFrom(const QString &name, const Profile &reference)
-{
- QString profileDirectory = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())).append("/").append(name);
- reference.copyFilesTo(profileDirectory);
- return Profile(QDir(profileDirectory));
-}
-
-
-Profile *Profile::createPtrFrom(const QString &name, const Profile &reference)
-{
- QString profileDirectory = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())).append("/").append(name);
- reference.copyFilesTo(profileDirectory);
- return new Profile(QDir(profileDirectory));
-}
-
-
-void Profile::copyFilesTo(QString &target) const
-{
- copyDir(m_Directory.absolutePath(), target, false);
-}
-
-
-std::vector<std::wstring> Profile::splitDZString(const wchar_t *buffer) const
-{
- std::vector<std::wstring> 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<wchar_t> 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<std::wstring> sections = splitDZString(buffer.data());
-
- // now iterate over all sections and retrieve a list of keys in each
- for (std::vector<std::wstring>::iterator iter = sections.begin();
- iter != sections.end(); ++iter) {
- // retrieve the names of all keys
- size = ::GetPrivateProfileStringW(iter->c_str(), NULL, NULL, 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<std::wstring> keys = splitDZString(buffer.data());
-
- for (std::vector<std::wstring>::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(),
- NULL, 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<QString> iniTweaks = modInfo->getIniTweaks();
- for (std::vector<QString>::iterator iter = iniTweaks.begin();
- iter != iniTweaks.end(); ++iter) {
- mergeTweak(*iter, tweakedIni);
- }
-}
-
-
-bool Profile::invalidationActive(bool *supported) const
-{
- if (GameInfo::instance().requiresBSAInvalidation()) {
- *supported = true;
- wchar_t buffer[1024];
- std::wstring iniFileName = ToWString(QDir::toNativeSeparators(getIniFileName()));
- // epic ms fail: GetPrivateProfileString uses errno (for whatever reason) to signal a fail since the return value
- // has a different meaning (number of bytes copied). HOWEVER, it will not set errno to 0 if NO error occured
- errno = 0;
- if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(),
- L"", buffer, 1024, iniFileName.c_str()) == 0) {
- if (errno != 0x02) {
- if (supported != NULL) {
- *supported = false;
- }
- return false;
- } else {
- QString errorMessage = tr("failed to parse ini file (%1)").arg(ToQString(iniFileName));
- throw windows_error(errorMessage.toUtf8().constData());
- }
- }
- QStringList archives = ToQString(buffer).split(',');
-
- for (int i = 0; i < archives.count(); ++i) {
- QString bsaName = archives.at(i).trimmed();
- if (GameInfo::instance().isInvalidationBSA(ToWString(bsaName))) {
- return true;
- }
- }
- } else {
- *supported = false;
- }
- return false;
-}
-
-
-void Profile::deactivateInvalidation() const
-{
- if (GameInfo::instance().requiresBSAInvalidation()) {
- wchar_t buffer[1024];
- std::wstring iniFileName = ToWString(QDir::toNativeSeparators(getIniFileName()));
- errno = 0;
- if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(),
- L"", buffer, 1024, iniFileName.c_str()) == 0) {
- if (errno == 0x02) {
- QString errorMessage = tr("failed to parse ini file (%1): %2").arg(QDir::toNativeSeparators(getIniFileName())).arg(::GetLastError());
- throw windows_error(errorMessage.toUtf8().constData());
- } else {
- return;
- }
- }
- QStringList archives = ToQString(buffer).split(", ");
-
- for (int i = 0; i < archives.count();) {
- QString bsaName = archives.at(i).trimmed();
- if (GameInfo::instance().isInvalidationBSA(ToWString(bsaName))) {
- archives.removeAt(i);
- } else {
- ++i;
- }
- }
-
- // just to be safe...
- ::SetFileAttributesW(iniFileName.c_str(), FILE_ATTRIBUTE_NORMAL);
-
- if (!::WritePrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(),
- ToWString(archives.join(", ").toUtf8()).c_str(), iniFileName.c_str()) ||
- !::WritePrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"0", iniFileName.c_str()) ||
- !::WritePrivateProfileStringW(L"Archive", L"SInvalidationFile", L"ArchiveInvalidation.txt", iniFileName.c_str())) {
- QString errorMessage = tr("failed to modify \"%1\"").arg(ToQString(iniFileName));
- throw windows_error(errorMessage.toUtf8().constData());
- }
- }
-}
-
-
-void Profile::activateInvalidation(const QString& dataDirectory) const
-{
- if (GameInfo::instance().requiresBSAInvalidation()) {
- wchar_t buffer[1024];
- std::wstring iniFileName = ToWString(QDir::toNativeSeparators(getIniFileName()));
- errno = 0;
- if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(),
- L"", buffer, 1024, iniFileName.c_str()) == 0) {
- if (errno == 0x02) {
- throw windows_error("failed to parse ini file");
- } else {
- // ignore. shouldn't have gotten here anyway
- return;
- }
- }
- QStringList archives = ToQString(buffer).split(", ");
-
- QString invalidationBSA = ToQString(GameInfo::instance().getInvalidationBSA());
-
- if (!archives.contains(invalidationBSA)) {
- archives.insert(0, invalidationBSA);
- }
-
- // just to be safe...
- ::SetFileAttributesW(iniFileName.c_str(), FILE_ATTRIBUTE_NORMAL);
-
- if (!::WritePrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(),
- ToWString(archives.join(", ").toUtf8()).c_str(), iniFileName.c_str()) ||
- !::WritePrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"1", iniFileName.c_str()) ||
- !::WritePrivateProfileStringW(L"Archive", L"SInvalidationFile", L"", iniFileName.c_str())) {
- QString errorMessage = tr("failed to modify \"%1\"").arg(ToQString(iniFileName));
- throw windows_error(errorMessage.toUtf8().constData());
- }
-
- QString bsaFile = dataDirectory + "/" + invalidationBSA;
- if (!QFile::exists(bsaFile)) {
- DummyBSA bsa;
- bsa.write(bsaFile);
- }
- }
-}
-
-
-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::getPath() const
-{
- return QDir::cleanPath(m_Directory.absolutePath());
-}
-
-void Profile::rename(const QString &newName)
-{
- QDir profileDir(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())));
- profileDir.rename(getName(), newName);
- m_Directory = profileDir.absoluteFilePath(newName);
-}
+/* +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 <http://www.gnu.org/licenses/>. +*/ + +#include "profile.h" +#include "report.h" +#include "gameinfo.h" +#include "windows_error.h" +#include "dummybsa.h" +#include "modinfo.h" +#include "safewritefile.h" +#include <utility.h> +#include <util.h> +#include <error_report.h> +#include <appconfig.h> +#include <QMessageBox> +#include <QApplication> +#include <QSettings> +#include <QTemporaryFile> + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <shlobj.h> +#include <stdexcept> + +using namespace MOBase; +using namespace MOShared; + +Profile::Profile() + : m_SaveTimer(NULL) +{ + initTimer(); +} + +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, bool useDefaultSettings) + : m_SaveTimer(NULL) +{ + initTimer(); + QString profilesDir = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())); + 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"); + + GameInfo::instance().createProfile(ToWString(fullPath), useDefaultSettings); + } catch (...) { + // clean up in case of an error + shellDelete(QStringList(profileBase.absoluteFilePath(fixedName))); + throw; + } + refreshModStatus(); +} + + +Profile::Profile(const QDir& directory) + : m_Directory(directory), m_SaveTimer(NULL) +{ + initTimer(); + if (!QFile::exists(m_Directory.filePath("modlist.txt"))) { + qWarning("missing modlist.txt in %s", qPrintable(directory.path())); + } + + GameInfo::instance().repairProfile(ToWString(m_Directory.absolutePath())); + + 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_SaveTimer(NULL) +{ + initTimer(); + refreshModStatus(); +} + + +Profile::~Profile() +{ + writeModlistNow(); +} + + +void Profile::initTimer() +{ + m_SaveTimer = new QTimer(this); + m_SaveTimer->setSingleShot(true); + connect(m_SaveTimer, SIGNAL(timeout()), this, SLOT(writeModlistNow())); +} + + +bool Profile::exists() const +{ + return m_Directory.exists(); +} + + +void Profile::writeModlist() const +{ + if (!m_SaveTimer->isActive()) { + m_SaveTimer->start(2000); + } +} + + +void Profile::cancelWriteModlist() const +{ + m_SaveTimer->stop(); +} + + +void Profile::writeModlistNow(bool onlyOnTimer) const +{ + if (onlyOnTimer && !m_SaveTimer->isActive()) return; + + m_SaveTimer->stop(); + 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<ModInfo::EFlag> 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) { + if (m_ModStatus[i].m_Enabled) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + 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"1", 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.exists()) { + throw MyException(tr("\"%1\" is missing or inaccessible").arg(getModlistFileName())); + } + + bool modStatusModified = false; + m_ModStatus.clear(); + m_ModStatus.resize(ModInfo::getNumMods()); + + std::set<QString> namesRead; + + // load mods from file and update enabled state and priority for them + file.open(QIODevice::ReadOnly); + int index = 0; + while (!file.atEnd()) { + QByteArray line = file.readLine(); + 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 + (line.at(0) == '*' ? ModInfoForeign::INT_IDENTIFIER : ""); + 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<size_t>(index) >= m_ModStatus.size()) { + throw MyException(tr("invalid index %1").arg(index)); + } + m_ModStatus[modIndex].m_Priority = index++; + } + } else { + qDebug("mod \"%s\" (profile \"%s\") not found", + modName.toUtf8().constData(), m_Directory.path().toUtf8().constData()); + // need to rewrite the modlist to fix this + modStatusModified = true; + } + } + } else { + // line was empty after trimming + } + } + + 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<size_t>(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) { + writeModlist(); + } +} + + +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<int>(m_ModIndexByPriority.size())) { + qCritical("invalid priority %d for mod", priority); + continue; + } else { + ++m_NumRegularMods; + m_ModIndexByPriority.at(priority) = i; + } + } +} + + +std::vector<std::tuple<QString, QString, int> > Profile::getActiveMods() +{ + std::vector<std::tuple<QString, QString, int> > result; + for (std::vector<unsigned int>::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<ModInfo::EFlag> 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<int>)(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(); + writeModlist(); +} + + +Profile Profile::createFrom(const QString &name, const Profile &reference) +{ + QString profileDirectory = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())).append("/").append(name); + reference.copyFilesTo(profileDirectory); + return Profile(QDir(profileDirectory)); +} + + +Profile *Profile::createPtrFrom(const QString &name, const Profile &reference) +{ + QString profileDirectory = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())).append("/").append(name); + reference.copyFilesTo(profileDirectory); + return new Profile(QDir(profileDirectory)); +} + + +void Profile::copyFilesTo(QString &target) const +{ + copyDir(m_Directory.absolutePath(), target, false); +} + + +std::vector<std::wstring> Profile::splitDZString(const wchar_t *buffer) const +{ + std::vector<std::wstring> 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<wchar_t> 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<std::wstring> sections = splitDZString(buffer.data()); + + // now iterate over all sections and retrieve a list of keys in each + for (std::vector<std::wstring>::iterator iter = sections.begin(); + iter != sections.end(); ++iter) { + // retrieve the names of all keys + size = ::GetPrivateProfileStringW(iter->c_str(), NULL, NULL, 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<std::wstring> keys = splitDZString(buffer.data()); + + for (std::vector<std::wstring>::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(), + NULL, 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<QString> iniTweaks = modInfo->getIniTweaks(); + for (std::vector<QString>::iterator iter = iniTweaks.begin(); + iter != iniTweaks.end(); ++iter) { + mergeTweak(*iter, tweakedIni); + } +} + + +bool Profile::invalidationActive(bool *supported) const +{ + if (GameInfo::instance().requiresBSAInvalidation()) { + *supported = true; + wchar_t buffer[1024]; + std::wstring iniFileName = ToWString(QDir::toNativeSeparators(getIniFileName())); + // epic ms fail: GetPrivateProfileString uses errno (for whatever reason) to signal a fail since the return value + // has a different meaning (number of bytes copied). HOWEVER, it will not set errno to 0 if NO error occured + errno = 0; + if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(), + L"", buffer, 1024, iniFileName.c_str()) == 0) { + if (errno != 0x02) { + if (supported != NULL) { + *supported = false; + } + return false; + } else { + QString errorMessage = tr("failed to parse ini file (%1)").arg(ToQString(iniFileName)); + throw windows_error(errorMessage.toUtf8().constData()); + } + } + QStringList archives = ToQString(buffer).split(','); + + for (int i = 0; i < archives.count(); ++i) { + QString bsaName = archives.at(i).trimmed(); + if (GameInfo::instance().isInvalidationBSA(ToWString(bsaName))) { + return true; + } + } + } else { + *supported = false; + } + return false; +} + + +void Profile::deactivateInvalidation() const +{ + if (GameInfo::instance().requiresBSAInvalidation()) { + wchar_t buffer[1024]; + std::wstring iniFileName = ToWString(QDir::toNativeSeparators(getIniFileName())); + errno = 0; + if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(), + L"", buffer, 1024, iniFileName.c_str()) == 0) { + if (errno == 0x02) { + QString errorMessage = tr("failed to parse ini file (%1): %2").arg(QDir::toNativeSeparators(getIniFileName())).arg(::GetLastError()); + throw windows_error(errorMessage.toUtf8().constData()); + } else { + return; + } + } + QStringList archives = ToQString(buffer).split(", "); + + for (int i = 0; i < archives.count();) { + QString bsaName = archives.at(i).trimmed(); + if (GameInfo::instance().isInvalidationBSA(ToWString(bsaName))) { + archives.removeAt(i); + } else { + ++i; + } + } + + // just to be safe... + ::SetFileAttributesW(iniFileName.c_str(), FILE_ATTRIBUTE_NORMAL); + + if (!::WritePrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(), + ToWString(archives.join(", ").toUtf8()).c_str(), iniFileName.c_str()) || + !::WritePrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"0", iniFileName.c_str()) || + !::WritePrivateProfileStringW(L"Archive", L"SInvalidationFile", L"ArchiveInvalidation.txt", iniFileName.c_str())) { + QString errorMessage = tr("failed to modify \"%1\"").arg(ToQString(iniFileName)); + throw windows_error(errorMessage.toUtf8().constData()); + } + } +} + + +void Profile::activateInvalidation(const QString& dataDirectory) const +{ + if (GameInfo::instance().requiresBSAInvalidation()) { + wchar_t buffer[1024]; + std::wstring iniFileName = ToWString(QDir::toNativeSeparators(getIniFileName())); + errno = 0; + if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(), + L"", buffer, 1024, iniFileName.c_str()) == 0) { + if (errno == 0x02) { + throw windows_error("failed to parse ini file"); + } else { + // ignore. shouldn't have gotten here anyway + return; + } + } + QStringList archives = ToQString(buffer).split(", "); + + QString invalidationBSA = ToQString(GameInfo::instance().getInvalidationBSA()); + + if (!archives.contains(invalidationBSA)) { + archives.insert(0, invalidationBSA); + } + + // just to be safe... + ::SetFileAttributesW(iniFileName.c_str(), FILE_ATTRIBUTE_NORMAL); + + if (!::WritePrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(), + ToWString(archives.join(", ").toUtf8()).c_str(), iniFileName.c_str()) || + !::WritePrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"1", iniFileName.c_str()) || + !::WritePrivateProfileStringW(L"Archive", L"SInvalidationFile", L"", iniFileName.c_str())) { + QString errorMessage = tr("failed to modify \"%1\"").arg(ToQString(iniFileName)); + throw windows_error(errorMessage.toUtf8().constData()); + } + + QString bsaFile = dataDirectory + "/" + invalidationBSA; + if (!QFile::exists(bsaFile)) { + DummyBSA bsa; + bsa.write(bsaFile); + } + } +} + + +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::getPath() const +{ + return QDir::cleanPath(m_Directory.absolutePath()); +} + +void Profile::rename(const QString &newName) +{ + QDir profileDir(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir()))); + profileDir.rename(getName(), newName); + m_Directory = profileDir.absoluteFilePath(newName); +} diff --git a/src/profile.h b/src/profile.h index fa34ab9e..aa27d6e8 100644 --- a/src/profile.h +++ b/src/profile.h @@ -1,330 +1,331 @@ -/*
-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 <http://www.gnu.org/licenses/>.
-*/
-
-#ifndef PROFILE_H
-#define PROFILE_H
-
-
-#include "modinfo.h"
-
-#include <QString>
-#include <QDir>
-#include <QMetaType>
-#include <QSettings>
-#include <vector>
-#include <tuple>
-
-
-/**
- * @brief represents a profile
- **/
-class Profile : public QObject
-{
-
- Q_OBJECT
-
-public:
-
- typedef boost::shared_ptr<Profile> Ptr;
-
-public:
-
- /**
- * @brief default constructor
- * @todo This constructor initialised nothing, the resulting object is not usable
- **/
- Profile();
-
- /**
- * @brief constructor
- *
- * This constructor is used to create a new profile so it is to be assumed a profile
- * by this name does not yet exist
- * @param name name of the new profile
- * @param filter save game filter. Defaults to <no filter>.
- **/
- Profile(const QString &name, bool useDefaultSettings);
- /**
- * @brief constructor
- *
- * This constructor is used to open an existing profile though it will also try to repair
- * the profile if important files are missing (including the directory itself) so technically,
- * invoking this should always produce a working profile
- * @param directory directory to read the profile from
- **/
- Profile(const QDir &directory);
-
- Profile(const Profile &reference);
-
- ~Profile();
-
- /**
- * @return true if this profile (still) exists on disc
- */
- bool exists() const;
-
- /**
- * @param name of the new profile
- * @param reference profile to copy from
- **/
- static Profile createFrom(const QString &name, const Profile &reference);
-
- /**
- * @param name of the new profile
- * @param reference profile to copy from
- **/
- static Profile *createPtrFrom(const QString &name, const Profile &reference);
-
- /**
- * @brief write out the modlist.txt
- **/
- void writeModlist() const;
-
- /**
- * cancel any potential request to write modlist.txt. This is used to ensure no invalid modlist is
- * saved while it's being modified
- */
- void cancelWriteModlist() const;
-
- /**
- * @brief test if this profile uses archive invalidation
- *
- * @param supported if this is not null, the parameter will be set to false if invalidation is not supported in this profile
- * @return true if archive invalidation is active
- * @note currently, invalidation is not supported if the relevant entry in the ini file does not exist
- **/
- bool invalidationActive(bool *supported) const;
-
- /**
- * @brief deactivate archive invalidation if it was active
- **/
- void deactivateInvalidation() const;
-
- /**
- * @brief activate archive invalidation
- *
- * @param dataDirectory data directory of the game
- * @todo passing the data directory as a parameter is useless, the function should
- * be able to query it from GameInfo
- **/
- void activateInvalidation(const QString &dataDirectory) const;
-
- /**
- * @return true if this profile uses local save games
- */
- bool localSavesEnabled() const;
-
- /**
- * @brief enables or disables the use of local save games for this profile
- * disabling this does not delete exising local saves but they will not be visible
- * in the game
- * @param enable if true, local saves are enabled, otherewise they are disabled
- */
- bool enableLocalSaves(bool enable);
-
- /**
- * @return name of the profile (this is identical to its directory name)
- **/
- QString getName() const { return m_Directory.dirName(); }
-
- /**
- * @return the path of the plugins file in this profile
- * @todo is this required? can the functionality using this function be moved to the Profile-class?
- **/
- QString getPluginsFileName() const;
-
- /**
- * @return the path of the loadorder file in this profile
- **/
- QString getLoadOrderFileName() const;
-
- /**
- * @return the path of the file containing locked mod indices
- */
- QString getLockedOrderFileName() const;
-
- /**
- * @return the path of the modlist file in this profile
- */
- QString getModlistFileName() const;
-
- /**
- * @return path of the archives file in this profile
- */
- QString getArchivesFileName() const;
-
- /**
- * @return the path of the delete file in this profile
- * @note the deleter file lists plugins that should be hidden from the game and other tools
- **/
- QString getDeleterFileName() const;
-
- /**
- * @return the path of the ini file in this profile
- * @todo since the game can contain multiple ini files (i.e. skyrim.ini skyrimprefs.ini)
- * the concept of this function is somewhat broken
- **/
- QString getIniFileName() const;
-
- /**
- * @return the path of the tweak ini in this profile
- */
- QString getProfileTweaks() const;
-
- /**
- * @return path to this profile
- **/
- QString getPath() const;
-
- void rename(const QString &newName);
-
- /**
- * @brief create the ini file to be used by the game
- *
- * the tweaked ini file constructed by this file is a merger
- * of the game-ini of this profile with ini tweaks applied */
- void createTweakedIniFile();
-
- /**
- * @brief re-read the modlist.txt and update the mod status from it
- **/
- void refreshModStatus();
-
- /**
- * @brief retrieve a list of mods that are enabled in this profile
- *
- * @return list of active mods sorted by priority (ascending). "first" is the mod name, "second" is its path
- **/
- std::vector<std::tuple<QString, QString, int> > getActiveMods();
-
- /**
- * retrieve the number of mods for which this object has status information.
- * This is usually the same as ModInfo::getNumMods() except between
- * calls to ModInfo::updateFromDisc() and the Profile::refreshModStatus()
- *
- * @return number of mods for which the profile has status information
- **/
- unsigned int numMods() const { return m_ModStatus.size(); }
-
- /**
- * @return the number of mods that can be enabled and where the priority can be modified
- */
- unsigned int numRegularMods() const { return m_NumRegularMods; }
-
- /**
- * @brief retrieve the mod index based on the priority
- *
- * @param priority priority to look up
- * @return the index of the mod
- * @throw std::exception an exception is thrown if there is no mod with the specified priority
- **/
- unsigned int modIndexByPriority(unsigned int priority) const;
-
- /**
- * @brief enable or disable a mod
- *
- * @param index index of the mod to enable/disable
- * @param enabled true if the mod is to be enabled, false if it is to be disabled
- **/
- void setModEnabled(unsigned int index, bool enabled);
-
- /**
- * change the priority of a mod. Of course this also changes the priority of other mods.
- * The priority of the mods in the range ]old, new priority] are shifted so that no gaps
- * are possible.
- *
- * @param index index of the mod to change
- * @param newPriority the new priority value
- *
- * @todo what happens if the new priority is outside the range?
- **/
- void setModPriority(unsigned int index, int &newPriority);
-
- /**
- * @brief determine if a mod is enabled
- *
- * @param index index of the mod to look up
- * @return true if the mod is enabled, false otherwise
- **/
- bool modEnabled(unsigned int index) const;
-
- /**
- * @brief query the priority of a mod
- *
- * @param index index of the mod to look up
- * @return priority of the specified mod
- **/
- int getModPriority(unsigned int index) const;
-
- void dumpModStatus() const;
-
-signals:
-
- /**
- * @brief emitted whenever the status (enabled/disabled) of a mod changed
- *
- * @param index index of the mod that changed
- **/
- void modStatusChanged(unsigned int index);
-
-public slots:
-
- void writeModlistNow(bool onlyOnTimer = false) const;
-
-private:
-
- class ModStatus {
- friend class Profile;
- public:
- ModStatus() : m_Overwrite(false), m_Enabled(false), m_Priority(-1) {}
- private:
- bool m_Overwrite;
- bool m_Enabled;
- int m_Priority;
- };
-
-private:
- Profile& operator=(const Profile &reference); // not implemented
-
- void initTimer();
-
- void updateIndices();
-
- void copyFilesTo(QString &target) const;
-
- std::vector<std::wstring> splitDZString(const wchar_t *buffer) const;
- void mergeTweak(const QString &tweakName, const QString &tweakedIni) const;
- void mergeTweaks(ModInfo::Ptr modInfo, const QString &tweakedIni) const;
-
-private:
-
- QDir m_Directory;
-
- mutable QByteArray m_LastModlistHash;
- std::vector<ModStatus> m_ModStatus;
- std::vector<unsigned int> m_ModIndexByPriority;
- unsigned int m_NumRegularMods;
-
- QTimer *m_SaveTimer;
-};
-
-Q_DECLARE_METATYPE(Profile)
-
-
-#endif // PROFILE_H
+/* +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 <http://www.gnu.org/licenses/>. +*/ + +#ifndef PROFILE_H +#define PROFILE_H + + +#include "modinfo.h" + +#include <QString> +#include <QDir> +#include <QMetaType> +#include <QSettings> +#include <vector> +#include <tuple> + + +/** + * @brief represents a profile + **/ +class Profile : public QObject +{ + + Q_OBJECT + +public: + + typedef boost::shared_ptr<Profile> Ptr; + +public: + + /** + * @brief default constructor + * @todo This constructor initialised nothing, the resulting object is not usable + **/ + Profile(); + + /** + * @brief constructor + * + * This constructor is used to create a new profile so it is to be assumed a profile + * by this name does not yet exist + * @param name name of the new profile + * @param filter save game filter. Defaults to <no filter>. + **/ + Profile(const QString &name, bool useDefaultSettings); + /** + * @brief constructor + * + * This constructor is used to open an existing profile though it will also try to repair + * the profile if important files are missing (including the directory itself) so technically, + * invoking this should always produce a working profile + * @param directory directory to read the profile from + **/ + Profile(const QDir &directory); + + Profile(const Profile &reference); + + ~Profile(); + + /** + * @return true if this profile (still) exists on disc + */ + bool exists() const; + + /** + * @param name of the new profile + * @param reference profile to copy from + **/ + static Profile createFrom(const QString &name, const Profile &reference); + + /** + * @param name of the new profile + * @param reference profile to copy from + **/ + static Profile *createPtrFrom(const QString &name, const Profile &reference); + + /** + * @brief write out the modlist.txt + **/ + void writeModlist() const; + + /** + * cancel any potential request to write modlist.txt. This is used to ensure no invalid modlist is + * saved while it's being modified + */ + void cancelWriteModlist() const; + + /** + * @brief test if this profile uses archive invalidation + * + * @param supported if this is not null, the parameter will be set to false if invalidation is not supported in this profile + * @return true if archive invalidation is active + * @note currently, invalidation is not supported if the relevant entry in the ini file does not exist + **/ + bool invalidationActive(bool *supported) const; + + /** + * @brief deactivate archive invalidation if it was active + **/ + void deactivateInvalidation() const; + + /** + * @brief activate archive invalidation + * + * @param dataDirectory data directory of the game + * @todo passing the data directory as a parameter is useless, the function should + * be able to query it from GameInfo + **/ + void activateInvalidation(const QString &dataDirectory) const; + + /** + * @return true if this profile uses local save games + */ + bool localSavesEnabled() const; + + /** + * @brief enables or disables the use of local save games for this profile + * disabling this does not delete exising local saves but they will not be visible + * in the game + * @param enable if true, local saves are enabled, otherewise they are disabled + */ + bool enableLocalSaves(bool enable); + + /** + * @return name of the profile (this is identical to its directory name) + **/ + QString getName() const { return m_Directory.dirName(); } + + /** + * @return the path of the plugins file in this profile + * @todo is this required? can the functionality using this function be moved to the Profile-class? + **/ + QString getPluginsFileName() const; + + /** + * @return the path of the loadorder file in this profile + **/ + QString getLoadOrderFileName() const; + + /** + * @return the path of the file containing locked mod indices + */ + QString getLockedOrderFileName() const; + + /** + * @return the path of the modlist file in this profile + */ + QString getModlistFileName() const; + + /** + * @return path of the archives file in this profile + */ + QString getArchivesFileName() const; + + /** + * @return the path of the delete file in this profile + * @note the deleter file lists plugins that should be hidden from the game and other tools + **/ + QString getDeleterFileName() const; + + /** + * @return the path of the ini file in this profile + * @todo since the game can contain multiple ini files (i.e. skyrim.ini skyrimprefs.ini) + * the concept of this function is somewhat broken + **/ + QString getIniFileName() const; + + /** + * @return the path of the tweak ini in this profile + */ + QString getProfileTweaks() const; + + /** + * @return path to this profile + **/ + QString getPath() const; + + void rename(const QString &newName); + + /** + * @brief create the ini file to be used by the game + * + * the tweaked ini file constructed by this file is a merger + * of the game-ini of this profile with ini tweaks applied */ + void createTweakedIniFile(); + + /** + * @brief re-read the modlist.txt and update the mod status from it + **/ + void refreshModStatus(); + + /** + * @brief retrieve a list of mods that are enabled in this profile + * + * @return list of active mods sorted by priority (ascending). "first" is the mod name, "second" is its path + **/ + std::vector<std::tuple<QString, QString, int> > getActiveMods(); + + /** + * retrieve the number of mods for which this object has status information. + * This is usually the same as ModInfo::getNumMods() except between + * calls to ModInfo::updateFromDisc() and the Profile::refreshModStatus() + * + * @return number of mods for which the profile has status information + **/ + unsigned int numMods() const { return m_ModStatus.size(); } + + /** + * @return the number of mods that can be enabled and where the priority can be modified + */ + unsigned int numRegularMods() const { return m_NumRegularMods; } + + /** + * @brief retrieve the mod index based on the priority + * + * @param priority priority to look up + * @return the index of the mod + * @throw std::exception an exception is thrown if there is no mod with the specified priority + **/ + unsigned int modIndexByPriority(unsigned int priority) const; + + /** + * @brief enable or disable a mod + * + * @param index index of the mod to enable/disable + * @param enabled true if the mod is to be enabled, false if it is to be disabled + **/ + void setModEnabled(unsigned int index, bool enabled); + + /** + * change the priority of a mod. Of course this also changes the priority of other mods. + * The priority of the mods in the range ]old, new priority] are shifted so that no gaps + * are possible. + * + * @param index index of the mod to change + * @param newPriority the new priority value + * + * @todo what happens if the new priority is outside the range? + **/ + void setModPriority(unsigned int index, int &newPriority); + + /** + * @brief determine if a mod is enabled + * + * @param index index of the mod to look up + * @return true if the mod is enabled, false otherwise + **/ + bool modEnabled(unsigned int index) const; + + /** + * @brief query the priority of a mod + * + * @param index index of the mod to look up + * @return priority of the specified mod + **/ + int getModPriority(unsigned int index) const; + + void dumpModStatus() const; + +signals: + + /** + * @brief emitted whenever the status (enabled/disabled) of a mod changed + * + * @param index index of the mod that changed + **/ + void modStatusChanged(unsigned int index); + +public slots: + + void writeModlistNow(bool onlyOnTimer = false) const; + +private: + + class ModStatus { + friend class Profile; + public: + ModStatus() : m_Overwrite(false), m_Enabled(false), m_Priority(-1) {} + private: + bool m_Overwrite; + bool m_Enabled; + int m_Priority; + }; + +private: + Profile& operator=(const Profile &reference); // not implemented + + void initTimer(); + + void updateIndices(); + + void copyFilesTo(QString &target) const; + + std::vector<std::wstring> splitDZString(const wchar_t *buffer) const; + void mergeTweak(const QString &tweakName, const QString &tweakedIni) const; + void mergeTweaks(ModInfo::Ptr modInfo, const QString &tweakedIni) const; + void touchFile(QString fileName); + +private: + + QDir m_Directory; + + mutable QByteArray m_LastModlistHash; + std::vector<ModStatus> m_ModStatus; + std::vector<unsigned int> m_ModIndexByPriority; + unsigned int m_NumRegularMods; + + QTimer *m_SaveTimer; +}; + +Q_DECLARE_METATYPE(Profile) + + +#endif // PROFILE_H |
