diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2019-11-25 04:01:32 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-11-25 04:01:32 -0500 |
| commit | 765c9667de25547929f8ff58e3eab7bd3aeb3cbf (patch) | |
| tree | b8dc9f70a3a812fa3eb59461ab3bd65916b4b6b8 | |
| parent | 5f165b80e8035bafa15fe5a534726733836a3dd7 (diff) | |
| parent | f1b621d0babd33537cde97fc9d53e0dfa0ad5ea5 (diff) | |
Merge pull request #901 from isanae/loot-rework
Loot rework
| -rw-r--r-- | src/CMakeLists.txt | 19 | ||||
| -rw-r--r-- | src/json.h | 190 | ||||
| -rw-r--r-- | src/loot.cpp | 847 | ||||
| -rw-r--r-- | src/loot.h | 133 | ||||
| -rw-r--r-- | src/lootdialog.cpp | 273 | ||||
| -rw-r--r-- | src/lootdialog.h | 78 | ||||
| -rw-r--r-- | src/lootdialog.ui | 241 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 228 | ||||
| -rw-r--r-- | src/mainwindow.h | 4 | ||||
| -rw-r--r-- | src/pluginlist.cpp | 690 | ||||
| -rw-r--r-- | src/pluginlist.h | 76 | ||||
| -rw-r--r-- | src/resources/markdown.html | 292 | ||||
| -rw-r--r-- | src/settings.cpp | 11 | ||||
| -rw-r--r-- | src/settings.h | 5 | ||||
| -rw-r--r-- | src/settingsdialog.ui | 136 | ||||
| -rw-r--r-- | src/settingsdialogdiagnostics.cpp | 36 | ||||
| -rw-r--r-- | src/settingsdialogdiagnostics.h | 3 |
17 files changed, 2699 insertions, 563 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f168ef36..e3a42409 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -142,6 +142,8 @@ SET(organizer_SRCS sanitychecks.cpp processrunner.cpp uilocker.cpp + loot.cpp + lootdialog.cpp shared/windows_error.cpp shared/error_report.cpp @@ -263,6 +265,9 @@ SET(organizer_HDRS colortable.h processrunner.h uilocker.h + loot.h + lootdialog.h + json.h shared/windows_error.h shared/error_report.h @@ -389,6 +394,11 @@ set(executables editexecutablesdialog ) +set(loot + loot + lootdialog +) + set(modinfo modinfo modinfobackup @@ -465,6 +475,7 @@ set(utilities shared/util usvfsconnector shared/windows_error + json ) set(widgets @@ -485,7 +496,7 @@ set(widgets ) set(src_filters - application core browser dialogs downloads env executables modinfo + application core browser dialogs downloads env executables loot modinfo modinfo\\dialog modlist plugins previews profiles settings settingsdialog utilities widgets ) @@ -566,11 +577,13 @@ INCLUDE_DIRECTORIES(${project_path}/uibase/src ${project_path}/bsatk/src ${project_path}/esptk/src ${project_path}/archive/src + ${project_path}/lootcli/include ${dependency_project_path}/usvfs/include ${project_path}/game_gamebryo/src/gamebryo ${project_path}/game_gamebryo/src/creation ${project_path}/game_features/src ${project_path}/githubpp/src + ${SPDLOG_ROOT}/include ${LZ4_ROOT}/lib) INCLUDE_DIRECTORIES(shared ${ZLIB_INCLUDE_DIRS}) @@ -667,4 +680,6 @@ INSTALL( ) # qdds.dll needs installing manually as Qt no longer ships with it by default. -INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats)
\ No newline at end of file +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) + +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/markdown.html DESTINATION bin/resources) diff --git a/src/json.h b/src/json.h new file mode 100644 index 00000000..d182f330 --- /dev/null +++ b/src/json.h @@ -0,0 +1,190 @@ +#ifndef MODORGANIZER_JSON_INCLUDED +#define MODORGANIZER_JSON_INCLUDED + +#include <log.h> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> + +namespace json +{ + +class failed {}; + + +namespace details +{ + +QString typeName(const QJsonValue& v) +{ + if (v.isUndefined()) { + return "undefined"; + } else if (v.isNull()) { + return "null"; + } else if (v.isArray()) { + return "an array"; + } else if (v.isBool()) { + return "a bool"; + } else if (v.isDouble()) { + return "a double"; + } else if (v.isObject()) { + return "an object"; + } else if (v.isString()) { + return "a string"; + } else { + return "an unknown type"; + } +} + +QString typeName(const QJsonDocument& doc) +{ + if (doc.isEmpty()) { + return "empty"; + } else if (doc.isNull()) { + return "null"; + } else if (doc.isArray()) { + return "an array"; + } else if (doc.isObject()) { + return "an object"; + } else { + return "an unknown type"; + } +} + + +template <class T> +T convert(const QJsonValue& v) = delete; + +template <> +bool convert<bool>(const QJsonValue& v) +{ + if (!v.isBool()) { + throw failed(); + } + + return v.toBool(); +} + +template <> +QJsonObject convert<QJsonObject>(const QJsonValue& v) +{ + if (!v.isObject()) { + throw failed(); + } + + return v.toObject(); +} + +template <> +QString convert<QString>(const QJsonValue& v) +{ + if (!v.isString()) { + throw failed(); + } + + return v.toString(); +} + +template <> +QJsonArray convert<QJsonArray>(const QJsonValue& v) +{ + if (!v.isArray()) { + throw failed(); + } + + return v.toArray(); +} + +template <> +qint64 convert<qint64>(const QJsonValue& v) +{ + if (!v.isDouble()) { + throw failed(); + } + + return static_cast<qint64>(v.toDouble()); +} + +} // namespace + + +template <class T> +T convert(const QJsonValue& value, const char* what) +{ + try + { + return details::convert<T>(value); + } + catch(failed&) + { + MOBase::log::error( + "'{}' is a {}, not a {}", + what, details::typeName(value), typeid(T).name); + + throw; + } +} + +template <class T> +T convertWarn(const QJsonValue& value, const char* what, T def={}) +{ + try + { + return details::convert<T>(value); + } + catch(failed&) + { + MOBase::log::warn( + "'{}' is a {}, not a {}", + what, details::typeName(value), typeid(T).name()); + + return def; + } +} + +template <class T> +T get(const QJsonObject& o, const char* e) +{ + if (!o.contains(e)) { + MOBase::log::error("property '{}' is missing", e); + throw failed(); + } + + return convert<T>(o[e], e); +} + +template <class T> +T getWarn(const QJsonObject& o, const char* e, T def={}) +{ + if (!o.contains(e)) { + MOBase::log::warn("property '{}' is missing", e); + return def; + } + + return convertWarn<T>(o[e], e); +} + +template <class T> +T getOpt(const QJsonObject& o, const char* e, T def={}) +{ + if (!o.contains(e)) { + return def; + } + + return convertWarn<T>(o[e], e); +} + + +template <class Value> +void requireObject(const Value& v, const char* what) +{ + if (!v.isObject()) { + MOBase::log::error("{} is {}, not an object", what, details::typeName(v)); + throw failed(); + } +} + +} // namespace + +#endif // MODORGANIZER_JSON_INCLUDED diff --git a/src/loot.cpp b/src/loot.cpp new file mode 100644 index 00000000..11eb4f4e --- /dev/null +++ b/src/loot.cpp @@ -0,0 +1,847 @@ +#include "loot.h" +#include "lootdialog.h" +#include "spawn.h" +#include "organizercore.h" +#include "json.h" +#include <log.h> +#include <report.h> + +using namespace MOBase; +using namespace json; + +static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json"); +static const DWORD PipeTimeout = 500; + + +class AsyncPipe +{ +public: + AsyncPipe() + : m_ioPending(false) + { + std::fill(std::begin(m_buffer), std::end(m_buffer), 0); + std::memset(&m_ov, 0, sizeof(m_ov)); + } + + env::HandlePtr create() + { + // creating pipe + env::HandlePtr out(createPipe()); + if (out.get() == INVALID_HANDLE_VALUE) { + return {}; + } + + HANDLE readEventHandle = ::CreateEvent(nullptr, TRUE, FALSE, nullptr); + + if (readEventHandle == NULL) { + const auto e = GetLastError(); + log::error("CreateEvent failed for loot, {}", formatSystemMessage(e)); + return {}; + } + + m_ov.hEvent = readEventHandle; + m_readEvent.reset(readEventHandle); + + return out; + } + + std::string read() + { + if (m_ioPending) { + return checkPending(); + } else { + return tryRead(); + } + } + +private: + static const std::size_t bufferSize = 50'000; + + env::HandlePtr m_stdout; + env::HandlePtr m_readEvent; + char m_buffer[bufferSize]; + OVERLAPPED m_ov; + bool m_ioPending; + + HANDLE createPipe() + { + static const wchar_t* PipeName = L"\\\\.\\pipe\\lootcli_pipe"; + + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + + env::HandlePtr pipe; + + // creating pipe + { + HANDLE pipeHandle = ::CreateNamedPipe( + PipeName, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, + 1, 50'000, 50'000, PipeTimeout, &sa); + + if (pipeHandle == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateNamedPipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + pipe.reset(pipeHandle); + } + + { + // duplicating the handle to read from it + HANDLE outputRead = INVALID_HANDLE_VALUE; + + const auto r = DuplicateHandle( + GetCurrentProcess(), pipe.get(), GetCurrentProcess(), &outputRead, + 0, TRUE, DUPLICATE_SAME_ACCESS); + + if (!r) { + const auto e = GetLastError(); + log::error("DuplicateHandle for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + m_stdout.reset(outputRead); + } + + + // creating handle to pipe which is passed to CreateProcess() + HANDLE outputWrite = ::CreateFileW( + PipeName, FILE_WRITE_DATA|SYNCHRONIZE, 0, + &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + + if (outputWrite == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateFileW for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + return outputWrite; + } + + std::string tryRead() + { + DWORD bytesRead = 0; + + if (!::ReadFile(m_stdout.get(), m_buffer, bufferSize, &bytesRead, &m_ov)) { + const auto e = GetLastError(); + + switch (e) + { + case ERROR_IO_PENDING: + { + m_ioPending = true; + break; + } + + case ERROR_BROKEN_PIPE: + { + // broken pipe probably means lootcli is finished + break; + } + + default: + { + log::error("{}", formatSystemMessage(e)); + break; + } + } + + return {}; + } + + return {m_buffer, m_buffer + bytesRead}; + } + + std::string checkPending() + { + DWORD bytesRead = 0; + + if (!::GetOverlappedResultEx(m_stdout.get(), &m_ov, &bytesRead, PipeTimeout, FALSE)) { + const auto e = GetLastError(); + + switch (e) + { + case ERROR_IO_INCOMPLETE: + { + break; + } + + case WAIT_TIMEOUT: + { + break; + } + + case ERROR_BROKEN_PIPE: + { + // broken pipe probably means lootcli is finished + break; + } + + default: + { + log::error("GetOverlappedResult failed, {}", formatSystemMessage(e)); + break; + } + } + + return {}; + } + + ::ResetEvent(m_readEvent.get()); + m_ioPending = false; + + return {m_buffer, m_buffer + bytesRead}; + } +}; + + + +log::Levels levelFromLoot(lootcli::LogLevels level) +{ + using LC = lootcli::LogLevels; + + switch (level) + { + case LC::Trace: // fall-through + case LC::Debug: + return log::Debug; + + case LC::Info: + return log::Info; + + case LC::Warning: + return log::Warning; + + case LC::Error: + return log::Error; + + default: + return log::Info; + } +} + + +QString Loot::Report::toMarkdown() const +{ + QString s; + + if (!messages.empty()) { + s += "### " + QObject::tr("General messages") + "\n"; + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + } + + if (!plugins.empty()) { + if (!s.isEmpty()) { + s += "\n"; + } + + s += "### " + QObject::tr("Plugins") + "\n"; + + for (auto&& p : plugins) { + const auto ps = p.toMarkdown(); + if (!ps.isEmpty()) { + s += ps + "\n"; + } + } + } + + if (s.isEmpty()) { + s += "**" + QObject::tr("No messages.") + "**"; + } + + s += stats.toMarkdown(); + + return s; +} + +QString Loot::Stats::toMarkdown() const +{ + return QString("`stats: %1s, lootcli %2, loot %3`") + .arg(QString::number(time / 1000.0, 'f', 2)) + .arg(lootcliVersion) + .arg(lootVersion); +} + +QString Loot::Plugin::toMarkdown() const +{ + QString s; + + if (!incompatibilities.empty()) { + s += " - **" + QObject::tr("Incompatibilities") + ": "; + + QString fs; + for (auto&& f : incompatibilities) { + if (!fs.isEmpty()) { + fs += ", "; + } + + fs += f.displayName.isEmpty() ? f.name : f.displayName; + } + + s += fs + "**\n"; + } + + if (!missingMasters.empty()) { + s += " - **" + QObject::tr("Missing masters") + ": "; + + QString ms; + for (auto&& m : missingMasters) { + if (!ms.isEmpty()) { + ms += ", "; + } + + ms += m; + } + + s += ms + "**\n"; + } + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + + for (auto&& d : dirty) { + s += " - " + d.toMarkdown(false) + "\n"; + } + + if (!s.isEmpty()) { + s = "#### " + name + "\n" + s; + } + + return s; +} + +QString Loot::Dirty::toString(bool isClean) const +{ + if (isClean) { + return QObject::tr("Verified clean by %1") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility); + } + + QString s = cleaningString(); + + if (!info.isEmpty()) { + s += " " + info; + } + + return s; +} + +QString Loot::Dirty::toMarkdown(bool isClean) const +{ + return toString(isClean); +} + +QString Loot::Dirty::cleaningString() const +{ + return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility) + .arg(itm) + .arg(deletedReferences) + .arg(deletedNavmesh); +} + +QString Loot::Message::toMarkdown() const +{ + QString s; + + switch (type) + { + case log::Error: + { + s += "**" + QObject::tr("Error") + "**: "; + break; + } + + case log::Warning: + { + s += "**" + QObject::tr("Warning") + "**: "; + break; + } + + default: + { + break; + } + } + + s += text; + + return s; +} + + +Loot::Loot() + : m_thread(nullptr), m_cancel(false), m_result(false) +{ +} + +Loot::~Loot() +{ + if (m_thread) { + m_thread->wait(); + } + + if (QFile::exists(LootReportPath)) { + log::debug("deleting temporary loot report '{}'", LootReportPath); + const auto r = shell::Delete(LootReportPath); + + if (!r) { + log::error( + "failed to remove temporary loot json report '{}': {}", + LootReportPath, r.toString()); + } + } +} + +bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) +{ + log::debug("starting loot"); + + m_pipe.reset(new AsyncPipe); + + env::HandlePtr stdoutHandle = m_pipe->create(); + if (!stdoutHandle) { + return false; + } + + // vfs + core.prepareVFS(); + + // spawning + if (!spawnLootcli(parent, core, didUpdateMasterList, std::move(stdoutHandle))) { + return false; + } + + // starting thread + log::debug("starting loot thread"); + m_thread.reset(QThread::create([&]{ lootThread(); })); + m_thread->start(); + + return true; +} + +bool Loot::spawnLootcli( + QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, + env::HandlePtr stdoutHandle) +{ + const auto logLevel = core.settings().diagnostics().lootLogLevel(); + + QStringList parameters; + parameters + << "--game" << core.managedGame()->gameShortName() + << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) + << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) + << "--logLevel" << QString::fromStdString(lootcli::logLevelToString(logLevel)) + << "--out" << QString("\"%1\"").arg(LootReportPath) + << "--language" << core.settings().interface().language(); + + if (didUpdateMasterList) { + parameters << "--skipUpdateMasterlist"; + } + + spawn::SpawnParameters sp; + sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); + sp.arguments = parameters.join(" "); + sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); + sp.hooked = true; + sp.stdOut = stdoutHandle.get(); + + HANDLE lootHandle = spawn::startBinary(parent, sp); + + if (lootHandle == INVALID_HANDLE_VALUE) { + emit log(log::Levels::Error, tr("failed to start loot")); + return false; + } + + m_lootProcess.reset(lootHandle); + + return true; +} + +void Loot::cancel() +{ + if (!m_cancel) { + log::debug("loot received cancel request"); + m_cancel = true; + } +} + +bool Loot::result() const +{ + return m_result; +} + +const QString& Loot::outPath() const +{ + return LootReportPath; +} + +const Loot::Report& Loot::report() const +{ + return m_report; +} + +void Loot::lootThread() +{ + ::SetThreadDescription(GetCurrentThread(), L"loot"); + + try + { + m_result = false; + + if (waitForCompletion()) { + m_result = true; + processOutputFile(); + } + } + catch(...) + { + log::error("unhandled exception in loot thread"); + } + + log::debug("finishing loot thread"); + emit finished(); +} + + +bool Loot::waitForCompletion() +{ + bool terminating = false; + + log::debug("loot thread waiting for completion on lootcli"); + + for (;;) { + DWORD res = WaitForSingleObject(m_lootProcess.get(), 100); + + if (res == WAIT_OBJECT_0) { + log::debug("lootcli has completed"); + // done + break; + } + + if (res == WAIT_FAILED) { + const auto e = GetLastError(); + log::error("failed to wait on loot process, {}", formatSystemMessage(e)); + return false; + } + + if (m_cancel) { + log::debug("terminating lootcli process"); + ::TerminateProcess(m_lootProcess.get(), 1); + + log::debug("waiting for loocli process to terminate"); + WaitForSingleObject(m_lootProcess.get(), INFINITE); + + log::debug("lootcli terminated"); + return false; + } + + processStdout(m_pipe->read()); + } + + if (m_cancel) { + return false; + } + + processStdout(m_pipe->read()); + + // checking exit code + DWORD exitCode = 0; + + if (!::GetExitCodeProcess(m_lootProcess.get(), &exitCode)) { + const auto e = GetLastError(); + log::error("failed to get exit code for loot, {}", formatSystemMessage(e)); + return false; + } + + if (exitCode != 0UL) { + emit log(log::Levels::Error, tr("Loot failed. Exit code was: %1").arg(exitCode)); + return false; + } + + return true; +} + +void Loot::processStdout(const std::string &lootOut) +{ + emit output(QString::fromStdString(lootOut)); + + m_outputBuffer += lootOut; + if (m_outputBuffer.empty()) { + return; + } + + std::size_t start = 0; + + for (;;) { + const auto newline = m_outputBuffer.find("\n", start); + if (newline == std::string::npos) { + break; + } + + const std::string_view line(m_outputBuffer.c_str() + start, newline - start); + const auto m = lootcli::parseMessage(line); + + if (m.type == lootcli::MessageType::None) { + log::error("unrecognised loot output: '{}'", line); + continue; + } + + processMessage(m); + + start = newline + 1; + } + + m_outputBuffer.erase(0, start); +} + +void Loot::processMessage(const lootcli::Message& m) +{ + switch (m.type) + { + case lootcli::MessageType::Log: + { + emit log(levelFromLoot(m.logLevel), QString::fromStdString(m.log)); + break; + } + + case lootcli::MessageType::Progress: + { + emit progress(m.progress); + break; + } + } +} + +void Loot::processOutputFile() +{ + log::debug("parsing json output file at '{}'", LootReportPath); + + QFile outFile(LootReportPath); + if (!outFile.open(QIODevice::ReadOnly)) { + emit log( + MOBase::log::Error, + QString("failed to open file, %1 (error %2)") + .arg(outFile.errorString()).arg(outFile.error())); + + return; + } + + QJsonParseError e; + const QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll(), &e); + if (doc.isNull()) { + emit log( + MOBase::log::Error, + QString("invalid json, %1 (error %2)") + .arg(e.errorString()).arg(e.error)); + + return; + } + + m_report = createReport(doc); +} + +Loot::Report Loot::createReport(const QJsonDocument& doc) const +{ + requireObject(doc, "root"); + + Report r; + const QJsonObject object = doc.object(); + + r.messages = reportMessages(getOpt<QJsonArray>(object, "messages")); + r.plugins = reportPlugins(getOpt<QJsonArray>(object, "plugins")); + r.stats = reportStats(getWarn<QJsonObject>(object, "stats")); + + return r; +} + +std::vector<Loot::Plugin> Loot::reportPlugins(const QJsonArray& plugins) const +{ + std::vector<Loot::Plugin> v; + + for (auto pluginValue : plugins) { + const auto o = convertWarn<QJsonObject>(pluginValue, "plugin"); + if (o.isEmpty()) { + continue; + } + + auto p = reportPlugin(o); + if (!p.name.isEmpty()) { + v.emplace_back(std::move(p)); + } + } + + return v; +} + +Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const +{ + Plugin p; + + p.name = getWarn<QString>(plugin, "name"); + if (p.name.isEmpty()) { + return {}; + } + + if (plugin.contains("incompatibilities")) { + p.incompatibilities = reportFiles(getOpt<QJsonArray>(plugin, "incompatibilities")); + } + + if (plugin.contains("messages")) { + p.messages = reportMessages(getOpt<QJsonArray>(plugin, "messages")); + } + + if (plugin.contains("dirty")) { + p.dirty = reportDirty(getOpt<QJsonArray>(plugin, "dirty")); + } + + if (plugin.contains("clean")) { + p.clean = reportDirty(getOpt<QJsonArray>(plugin, "clean")); + } + + if (plugin.contains("missingMasters")) { + p.missingMasters = reportStringArray(getOpt<QJsonArray>(plugin, "missingMasters")); + } + + p.loadsArchive = getOpt(plugin, "loadsArchive", false); + p.isMaster = getOpt(plugin, "isMaster", false); + p.isLightMaster = getOpt(plugin, "isLightMaster", false); + + return p; +} + +Loot::Stats Loot::reportStats(const QJsonObject& stats) const +{ + Stats s; + + s.time = getWarn<qint64>(stats, "time"); + s.lootcliVersion = getWarn<QString>(stats, "lootcliVersion"); + s.lootVersion = getWarn<QString>(stats, "lootVersion"); + + return s; +} + +std::vector<Loot::Message> Loot::reportMessages(const QJsonArray& array) const +{ + std::vector<Loot::Message> v; + + for (auto messageValue : array) { + const auto o = convertWarn<QJsonObject>(messageValue, "message"); + if (o.isEmpty()) { + continue; + } + + Message m; + + const auto type = getWarn<QString>(o, "type"); + + if (type == "info") { + m.type = log::Info; + } else if (type == "warn") { + m.type = log::Warning; + } else if (type == "error") { + m.type = log::Error; + } else { + log::error("unknown message type '{}'", type); + m.type = log::Info; + } + + m.text = getWarn<QString>(o, "text"); + + if (!m.text.isEmpty()) { + v.emplace_back(std::move(m)); + } + } + + return v; +} + +std::vector<Loot::File> Loot::reportFiles(const QJsonArray& array) const +{ + std::vector<Loot::File> v; + + for (auto&& fileValue : array) { + const auto o = convertWarn<QJsonObject>(fileValue, "file"); + if (o.isEmpty()) { + continue; + } + + File f; + + f.name = getWarn<QString>(o, "name"); + f.displayName = getOpt<QString>(o, "displayName"); + + if (!f.name.isEmpty()) { + v.emplace_back(std::move(f)); + } + } + + return v; +} + +std::vector<Loot::Dirty> Loot::reportDirty(const QJsonArray& array) const +{ + std::vector<Loot::Dirty> v; + + for (auto&& dirtyValue : array) { + const auto o = convertWarn<QJsonObject>(dirtyValue, "dirty"); + + Dirty d; + + d.crc = getWarn<qint64>(o, "crc"); + d.itm = getOpt<qint64>(o, "itm"); + d.deletedReferences = getOpt<qint64>(o, "deletedReferences"); + d.deletedNavmesh = getOpt<qint64>(o, "deletedNavmesh"); + d.cleaningUtility = getOpt<QString>(o, "cleaningUtility"); + d.info = getOpt<QString>(o, "info"); + + v.emplace_back(std::move(d)); + } + + return v; +} + +std::vector<QString> Loot::reportStringArray(const QJsonArray& array) const +{ + std::vector<QString> v; + + for (auto&& sv : array) { + auto s = convertWarn<QString>(sv, "string"); + if (s.isEmpty()) { + continue; + } + + v.emplace_back(std::move(s)); + } + + return v; +} + + +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) +{ + core.savePluginList(); + + try { + Loot loot; + LootDialog dialog(parent, core, loot); + + loot.start(parent, core, didUpdateMasterList); + dialog.exec(); + + return dialog.result(); + } catch (const UsvfsConnectorException &e) { + log::debug("{}", e.what()); + return false; + } catch (const std::exception &e) { + reportError(QObject::tr("failed to run loot: %1").arg(e.what())); + return false; + } +} diff --git a/src/loot.h b/src/loot.h new file mode 100644 index 00000000..7e5e01bd --- /dev/null +++ b/src/loot.h @@ -0,0 +1,133 @@ +#ifndef MODORGANIZER_LOOT_H +#define MODORGANIZER_LOOT_H + +#include "envmodule.h" +#include <log.h> +#include <lootcli/lootcli.h> +#include <windows.h> +#include <QWidget> + +Q_DECLARE_METATYPE(lootcli::Progress); +Q_DECLARE_METATYPE(MOBase::log::Levels); + +class OrganizerCore; +class AsyncPipe; + +class Loot : public QObject +{ + Q_OBJECT; + +public: + struct Message + { + MOBase::log::Levels type; + QString text; + + QString toMarkdown() const; + }; + + struct File + { + QString name; + QString displayName; + }; + + struct Dirty + { + qint64 crc=0; + qint64 itm=0; + qint64 deletedReferences=0; + qint64 deletedNavmesh=0; + QString cleaningUtility; + QString info; + + QString toString(bool isClean) const; + QString toMarkdown(bool isClean) const; + QString cleaningString() const; + }; + + struct Plugin + { + QString name; + std::vector<File> incompatibilities; + std::vector<Message> messages; + std::vector<Dirty> dirty, clean; + std::vector<QString> missingMasters; + bool loadsArchive = false; + bool isMaster = false; + bool isLightMaster = false; + + QString toMarkdown() const; + }; + + struct Stats + { + qint64 time = 0; + QString lootcliVersion; + QString lootVersion; + + QString toMarkdown() const; + }; + + struct Report + { + std::vector<Message> messages; + std::vector<Plugin> plugins; + Stats stats; + + QString toMarkdown() const; + }; + + + Loot(); + ~Loot(); + + bool start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + void cancel(); + bool result() const; + const QString& outPath() const; + const Report& report() const; + +signals: + void output(const QString& s); + void progress(const lootcli::Progress p); + void log(MOBase::log::Levels level, const QString& s); + void finished(); + +private: + std::unique_ptr<QThread> m_thread; + std::atomic<bool> m_cancel; + std::atomic<bool> m_result; + env::HandlePtr m_lootProcess; + std::unique_ptr<AsyncPipe> m_pipe; + std::string m_outputBuffer; + Report m_report; + + bool spawnLootcli( + QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, + env::HandlePtr stdoutHandle); + + void lootThread(); + bool waitForCompletion(); + + void processStdout(const std::string &lootOut); + void processMessage(const lootcli::Message& m); + + void processOutputFile(); + + Report createReport(const QJsonDocument& doc) const; + Message reportMessage(const QJsonObject& message) const; + std::vector<Plugin> reportPlugins(const QJsonArray& plugins) const; + Loot::Plugin reportPlugin(const QJsonObject& plugin) const; + Loot::Stats reportStats(const QJsonObject& stats) const; + + std::vector<Message> reportMessages(const QJsonArray& array) const; + std::vector<Loot::File> reportFiles(const QJsonArray& array) const; + std::vector<Loot::Dirty> reportDirty(const QJsonArray& array) const; + std::vector<QString> reportStringArray(const QJsonArray& array) const; +}; + + +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + +#endif // MODORGANIZER_LOOT_H diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp new file mode 100644 index 00000000..9e269fef --- /dev/null +++ b/src/lootdialog.cpp @@ -0,0 +1,273 @@ +#include "lootdialog.h" +#include "ui_lootdialog.h" +#include "loot.h" +#include "organizercore.h" +#include <utility.h> +#include <QWebChannel> + +using namespace MOBase; + +QString progressToString(lootcli::Progress p) +{ + using P = lootcli::Progress; + + static const std::map<P, QString> map = { + {P::CheckingMasterlistExistence, QObject::tr("Checking masterlist existence")}, + {P::UpdatingMasterlist, QObject::tr("Updating masterlist")}, + {P::LoadingLists, QObject::tr("Loading lists")}, + {P::ReadingPlugins, QObject::tr("Reading plugins")}, + {P::SortingPlugins, QObject::tr("Sorting plugins")}, + {P::WritingLoadorder, QObject::tr("Writing loadorder.txt")}, + {P::ParsingLootMessages, QObject::tr("Parsing loot messages")}, + {P::Done, QObject::tr("Done")} + }; + + auto itor = map.find(p); + if (itor == map.end()) { + return QString("unknown progress %1").arg(static_cast<int>(p)); + } else { + return itor->second; + } +} + + +MarkdownDocument::MarkdownDocument(QObject* parent) + : QObject(parent) +{ +} + +void MarkdownDocument::setText(const QString& text) +{ + if (m_text == text) + return; + + m_text = text; + emit textChanged(m_text); +} + + +MarkdownPage::MarkdownPage(QObject* parent) + : QWebEnginePage(parent) +{ +} + +bool MarkdownPage::acceptNavigationRequest(const QUrl &url, NavigationType, bool) +{ + static const QStringList allowed = {"qrc", "data"}; + + if (!allowed.contains(url.scheme())) { + QDesktopServices::openUrl(url); + return false; + } + + return true; +} + + +LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : + QDialog(parent), ui(new Ui::LootDialog), m_core(core), m_loot(loot), + m_finished(false), m_cancelling(false) +{ + createUI(); + + QObject::connect( + &m_loot, &Loot::output, this, + [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::progress, + this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::log, this, + [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::finished, this, + [&]{ onFinished(); }, Qt::QueuedConnection); +} + +LootDialog::~LootDialog() = default; + +void LootDialog::setText(const QString& s) +{ + ui->progressText->setText(s); +} + +void LootDialog::setProgress(lootcli::Progress p) +{ + // don't overwrite the "stopping loot" message even if lootcli generates a new + // progress message + if (!m_cancelling) { + setText(progressToString(p)); + } + + if (p == lootcli::Progress::Done) { + ui->progressBar->setRange(0, 1); + ui->progressBar->setValue(1); + } +} + +void LootDialog::addOutput(const QString& s) +{ + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + return; + } + + const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); + + for (auto&& line : lines) { + if (line.isEmpty()) { + continue; + } + + addLineOutput(line); + } +} + +bool LootDialog::result() const +{ + return m_loot.result(); +} + +void LootDialog::cancel() +{ + if (!m_finished && !m_cancelling) { + log::debug("loot dialog: cancelling"); + m_loot.cancel(); + + setText(tr("Stopping LOOT...")); + addLineOutput("stopping loot"); + + ui->buttons->setEnabled(false); + m_cancelling = true; + } +} + +void LootDialog::openReport() +{ + const auto path = m_loot.outPath(); + shell::Open(path); +} + +int LootDialog::exec() +{ + auto& s = m_core.settings(); + + GeometrySaver gs(s, this); + s.geometry().restoreState(&m_expander); + + const auto r = QDialog::exec(); + + s.geometry().saveState(&m_expander); + + return r; +} + +void LootDialog::accept() +{ + // no-op +} + +void LootDialog::reject() +{ + if (m_finished) { + log::debug("loot dialog reject: loot finished, closing"); + QDialog::reject(); + } else { + log::debug("loot dialog reject: not finished, cancelling"); + cancel(); + } +} + +void LootDialog::createUI() +{ + ui->setupUi(this); + ui->progressBar->setMaximum(0); + + auto* page = new MarkdownPage(this); + ui->report->setPage(page); + + auto* channel = new QWebChannel(this); + channel->registerObject("content", &m_report); + page->setWebChannel(channel); + + const QString path = QApplication::applicationDirPath() + "/resources/markdown.html"; + QFile f(path); + + if (f.open(QFile::ReadOnly)) { + const QString html = f.readAll(); + if (!html.isEmpty()) { + ui->report->setHtml(html); + } else { + log::error("failed to read '{}', {}", path, f.errorString()); + } + } else { + log::error("can't open '{}', {}", path, f.errorString()); + } + + m_expander.set(ui->details, ui->detailsPanel); + ui->openJsonReport->setEnabled(false); + connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); }); + + ui->buttons->setStandardButtons(QDialogButtonBox::Cancel); + + resize(650, 450); +} + +void LootDialog::closeEvent(QCloseEvent* e) +{ + if (m_finished) { + log::debug("loot dialog close event: finished, closing"); + QDialog::closeEvent(e); + } else { + log::debug("loot dialog close event: not finished, cancelling"); + cancel(); + e->ignore(); + } +} + +void LootDialog::addLineOutput(const QString& line) +{ + ui->output->appendPlainText(line); +} + +void LootDialog::onFinished() +{ + log::debug("loot dialog: loot is finished"); + + m_finished = true; + + if (m_cancelling) { + log::debug("loot dialog: was cancelling, closing"); + close(); + } else { + log::debug("loot dialog: showing report"); + showReport(); + ui->openJsonReport->setEnabled(true); + ui->buttons->setStandardButtons(QDialogButtonBox::Close); + } +} + +void LootDialog::log(log::Levels lv, const QString& s) +{ + if (lv >= log::Levels::Warning) { + log::log(lv, "{}", s); + } + + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); + } +} + +void LootDialog::showReport() +{ + const auto& lootReport = m_loot.report(); + + m_core.pluginList()->clearAdditionalInformation(); + for (auto&& p : lootReport.plugins) { + m_core.pluginList()->addLootReport(p.name, p); + } + + m_report.setText(lootReport.toMarkdown()); +} diff --git a/src/lootdialog.h b/src/lootdialog.h new file mode 100644 index 00000000..bc8c01fb --- /dev/null +++ b/src/lootdialog.h @@ -0,0 +1,78 @@ +#ifndef MODORGANIZER_LOOTDIALOG_H +#define MODORGANIZER_LOOTDIALOG_H + +#include <lootcli/lootcli.h> +#include <log.h> +#include <expanderwidget.h> + +namespace Ui { class LootDialog; } + +class OrganizerCore; +class Loot; + + +class MarkdownDocument : public QObject +{ + Q_OBJECT; + Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged FINAL); + +public: + explicit MarkdownDocument(QObject* parent=nullptr); + void setText(const QString& text); + +signals: + void textChanged(const QString &text); + +private: + QString m_text; +}; + + +class MarkdownPage : public QWebEnginePage +{ + Q_OBJECT; + +public: + explicit MarkdownPage(QObject* parent=nullptr); + +protected: + bool acceptNavigationRequest(const QUrl &url, NavigationType, bool) override; +}; + + +class LootDialog : public QDialog +{ +public: + LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot); + ~LootDialog(); + + void setText(const QString& s); + void setProgress(lootcli::Progress p); + + void addOutput(const QString& s); + bool result() const; + void cancel(); + void openReport(); + + int exec() override; + void accept() override; + void reject() override; + +private: + std::unique_ptr<Ui::LootDialog> ui; + MOBase::ExpanderWidget m_expander; + OrganizerCore& m_core; + Loot& m_loot; + bool m_finished; + bool m_cancelling; + MarkdownDocument m_report; + + void createUI(); + void closeEvent(QCloseEvent* e) override; + void addLineOutput(const QString& line); + void onFinished(); + void log(MOBase::log::Levels lv, const QString& s); + void showReport(); +}; + +#endif // MODORGANIZER_LOOTDIALOG_H diff --git a/src/lootdialog.ui b/src/lootdialog.ui new file mode 100644 index 00000000..e366ce41 --- /dev/null +++ b/src/lootdialog.ui @@ -0,0 +1,241 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LootDialog</class> + <widget class="QDialog" name="LootDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>457</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>LOOT</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5" stretch="1,0"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="widget_2" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,1"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="progressText"> + <property name="text"> + <string>Progress</string> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>24</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QFrame" name="frame"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWebEngineView" name="report"> + <property name="url"> + <url> + <string>about:blank</string> + </url> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_3" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QToolButton" name="details"> + <property name="text"> + <string>Details</string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="detailsPanel" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="widget_5" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="openJsonReport"> + <property name="text"> + <string>Open JSON report</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="output"/> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttons"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>QWebEngineView</class> + <extends>QWidget</extends> + <header location="global">QtWebEngineWidgets/QWebEngineView</header> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>buttons</sender> + <signal>accepted()</signal> + <receiver>LootDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttons</sender> + <signal>rejected()</signal> + <receiver>LootDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9453f07c..9ea554a2 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -39,7 +39,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "spawn.h" #include "versioninfo.h" #include "instancemanager.h" - #include "report.h" #include "modlist.h" #include "modlistsortproxy.h" @@ -191,6 +190,8 @@ const QSize SmallToolbarSize(24, 24); const QSize MediumToolbarSize(32, 32); const QSize LargeToolbarSize(42, 36); +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + MainWindow::MainWindow(Settings &settings , OrganizerCore &organizerCore @@ -6369,233 +6370,14 @@ void MainWindow::on_showHiddenBox_toggled(bool checked) m_OrganizerCore.downloadManager()->setShowHidden(checked); } - -void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) -{ - SECURITY_ATTRIBUTES secAttributes; - secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - secAttributes.bInheritHandle = TRUE; - secAttributes.lpSecurityDescriptor = nullptr; - - if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { - log::error("failed to create stdout reroute"); - } - - if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { - log::error("failed to correctly set up the stdout reroute"); - *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; - } -} - -std::string MainWindow::readFromPipe(HANDLE stdOutRead) -{ - static const int chunkSize = 128; - std::string result; - - char buffer[chunkSize + 1]; - buffer[chunkSize] = '\0'; - - DWORD read = 1; - while (read > 0) { - if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) { - break; - } - if (read > 0) { - result.append(buffer, read); - if (read < chunkSize) { - break; - } - } - } - return result; -} - -void MainWindow::processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog) -{ - std::vector<std::string> lines; - boost::split(lines, lootOut, boost::is_any_of("\r\n")); - - std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); - std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); - - for (const std::string &line : lines) { - if (line.length() > 0) { - size_t progidx = line.find("[progress]"); - size_t erroridx = line.find("[error]"); - if (progidx != std::string::npos) { - dialog.setLabelText(line.substr(progidx + 11).c_str()); - } else if (erroridx != std::string::npos) { - log::warn("{}", line); - errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); - } else { - std::smatch match; - if (std::regex_match(line, match, exRequires)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("depends on missing \"%1\"").arg(dependency.c_str())); - } else if (std::regex_match(line, match, exIncompatible)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("incompatible with \"%1\"").arg(dependency.c_str())); - } else { - log::debug("[loot] {}", line); - } - } - } - } -} - void MainWindow::on_bossButton_clicked() { - std::string errorMessages; - - //m_OrganizerCore.currentProfile()->writeModlistNow(); m_OrganizerCore.savePluginList(); - //Create a backup of the load orders w/ LOOT in name - //to make sure that any sorting is easily undo-able. - //Need to figure out how I want to do that. - - bool success = false; - - try { - setEnabled(false); - ON_BLOCK_EXIT([&] () { setEnabled(true); }); - QProgressDialog dialog(this); - dialog.setLabelText(tr("Please wait while LOOT is running")); - dialog.setMaximum(0); - dialog.show(); - - QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); - - QStringList parameters; - parameters << "--game" << m_OrganizerCore.managedGame()->gameShortName() - << "--gamePath" << QString("\"%1\"").arg(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()) - << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(m_OrganizerCore.profilePath()) - << "--out" << QString("\"%1\"").arg(outPath); - if (m_DidUpdateMasterList) { - parameters << "--skipUpdateMasterlist"; - } - HANDLE stdOutWrite = INVALID_HANDLE_VALUE; - HANDLE stdOutRead = INVALID_HANDLE_VALUE; - createStdoutPipe(&stdOutRead, &stdOutWrite); - try { - m_OrganizerCore.prepareVFS(); - } catch (const UsvfsConnectorException &e) { - log::debug("{}", e.what()); - return; - } catch (const std::exception &e) { - QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what()); - return; - } - - spawn::SpawnParameters sp; - sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); - sp.arguments = parameters.join(" "); - sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); - sp.hooked = true; - sp.stdOut = stdOutWrite; - - HANDLE loot = spawn::startBinary(this, sp); - - // we don't use the write end - ::CloseHandle(stdOutWrite); - - m_OrganizerCore.pluginList()->clearAdditionalInformation(); - - DWORD retLen; - JOBOBJECT_BASIC_PROCESS_ID_LIST info; - HANDLE processHandle = loot; - - if (loot != INVALID_HANDLE_VALUE) { - bool isJobHandle = true; - ULONG lastProcessID; - DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); - while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { - if (isJobHandle) { - if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { - if (info.NumberOfProcessIdsInList == 0) { - log::debug("no more processes in job"); - break; - } else { - if (lastProcessID != info.ProcessIdList[0]) { - lastProcessID = info.ProcessIdList[0]; - if (processHandle != loot) { - ::CloseHandle(processHandle); - } - processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); - } - } - } else { - // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there - // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running. - // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without - // the right to break out. - if (::GetLastError() != ERROR_MORE_DATA) { - isJobHandle = false; - } - } - } - - if (dialog.wasCanceled()) { - if (isJobHandle) { - ::TerminateJobObject(loot, 1); - } else { - ::TerminateProcess(loot, 1); - } - } - - // keep processing events so the app doesn't appear dead - QCoreApplication::processEvents(); - std::string lootOut = readFromPipe(stdOutRead); - processLOOTOut(lootOut, errorMessages, dialog); - - res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); - } - - std::string remainder = readFromPipe(stdOutRead).c_str(); - if (remainder.length() > 0) { - processLOOTOut(remainder, errorMessages, dialog); - } - DWORD exitCode = 0UL; - ::GetExitCodeProcess(processHandle, &exitCode); - ::CloseHandle(processHandle); - if (exitCode != 0UL) { - reportError(tr("loot failed. Exit code was: %1").arg(exitCode)); - return; - } else { - success = true; - QFile outFile(outPath); - outFile.open(QIODevice::ReadOnly); - QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); - QJsonArray array = doc.array(); - for (auto iter = array.begin(); iter != array.end(); ++iter) { - QJsonObject pluginObj = (*iter).toObject(); - QJsonArray pluginMessages = pluginObj["messages"].toArray(); - for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { - QJsonObject msg = (*msgIter).toObject(); - m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), - QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); - } - if (pluginObj["dirty"].toString() == "yes") - m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), "dirty"); - } - - } - } else { - reportError(tr("failed to start loot")); - } - } catch (const std::exception &e) { - reportError(tr("failed to run loot: %1").arg(e.what())); - } - - if (errorMessages.length() > 0) { - QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occurred"), errorMessages.c_str(), QMessageBox::Ok, this); - warn->setModal(false); - warn->show(); - } + setEnabled(false); + ON_BLOCK_EXIT([&] () { setEnabled(true); }); - if (success) { + if (runLoot(this, m_OrganizerCore, m_DidUpdateMasterList)) { m_DidUpdateMasterList = true; m_OrganizerCore.refreshESPList(false); m_OrganizerCore.savePluginList(); diff --git a/src/mainwindow.h b/src/mainwindow.h index c80287b2..7c2ee1eb 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -136,10 +136,6 @@ public: void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info); - void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite); - std::string readFromPipe(HANDLE stdOutRead); - void processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog); - void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo); QString getOriginDisplayName(int originID); diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index f35f9409..e91d820d 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -56,33 +56,40 @@ using namespace MOBase; using namespace MOShared;
-static bool ByName(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) {
- return LHS.m_Name.toUpper() < RHS.m_Name.toUpper();
+static bool ByName(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS)
+{
+ return LHS.name.toUpper() < RHS.name.toUpper();
}
-static bool ByPriority(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) {
- if (LHS.m_IsMaster && !RHS.m_IsMaster) {
+static bool ByPriority(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS)
+{
+ if (LHS.isMaster && !RHS.isMaster) {
return true;
- } else if (!LHS.m_IsMaster && RHS.m_IsMaster) {
+ } else if (!LHS.isMaster && RHS.isMaster) {
return false;
} else {
- return LHS.m_Priority < RHS.m_Priority;
+ return LHS.priority < RHS.priority;
}
}
-static bool ByDate(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) {
- return QFileInfo(LHS.m_FullPath).lastModified() < QFileInfo(RHS.m_FullPath).lastModified();
+static bool ByDate(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS)
+{
+ return QFileInfo(LHS.fullPath).lastModified() < QFileInfo(RHS.fullPath).lastModified();
}
-static QString TruncateString(const QString& text) {
+static QString TruncateString(const QString& text)
+{
QString new_text = text;
+
if (new_text.length() > 1024) {
new_text.truncate(1024);
new_text += "...";
}
+
return new_text;
}
+
PluginList::PluginList(QObject *parent)
: QAbstractItemModel(parent)
, m_FontMetrics(QFont())
@@ -125,8 +132,9 @@ QString PluginList::getColumnToolTip(int column) void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry, const Profile &profile)
{
for (auto &esp : m_ESPs) {
- esp.m_ModSelected = false;
+ esp.modSelected = false;
}
+
for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) {
int modIndex = idx.data(Qt::UserRole + 1).toInt();
if (modIndex == UINT_MAX)
@@ -147,12 +155,13 @@ void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MO }
std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin.toLower());
if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].m_ModSelected = true;
+ m_ESPs[iter->second].modSelected = true;
}
}
}
}
}
+
emit dataChanged(this->index(0, 0), this->index(static_cast<int>(m_ESPs.size()) - 1, this->columnCount() - 1));
}
@@ -225,7 +234,7 @@ void PluginList::refresh(const QString &profileName }
m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives, lightPluginsAreSupported));
- m_ESPs.rbegin()->m_Priority = -1;
+ m_ESPs.rbegin()->priority = -1;
} catch (const std::exception &e) {
reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what()));
}
@@ -234,13 +243,13 @@ void PluginList::refresh(const QString &profileName for (const auto &espName : m_ESPsByName) {
if (!availablePlugins.contains(espName.first)) {
- m_ESPs[espName.second].m_Name = "";
+ m_ESPs[espName.second].name = "";
}
}
m_ESPs.erase(std::remove_if(m_ESPs.begin(), m_ESPs.end(),
[](const ESPInfo &info) -> bool {
- return info.m_Name.isEmpty();
+ return info.name.isEmpty();
}),
m_ESPs.end());
@@ -273,7 +282,7 @@ void PluginList::fixPriorities() std::vector<std::pair<int, int>> espPrios;
for (int i = 0; i < m_ESPs.size(); ++i) {
- int prio = m_ESPs[i].m_Priority;
+ int prio = m_ESPs[i].priority;
if (prio == -1) {
prio = INT_MAX;
}
@@ -286,7 +295,7 @@ void PluginList::fixPriorities() });
for (int i = 0; i < espPrios.size(); ++i) {
- m_ESPs[espPrios[i].second].m_Priority = i;
+ m_ESPs[espPrios[i].second].priority = i;
}
}
@@ -295,8 +304,8 @@ void PluginList::enableESP(const QString &name, bool enable) std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].m_Enabled =
- enable | m_ESPs[iter->second].m_ForceEnabled;
+ m_ESPs[iter->second].enabled =
+ enable | m_ESPs[iter->second].forceEnabled;
emit writePluginsList();
} else {
@@ -307,7 +316,7 @@ void PluginList::enableESP(const QString &name, bool enable) int PluginList::findPluginByPriority(int priority)
{
for (int i = 0; i < m_ESPs.size(); i++ ) {
- if (m_ESPs[i].m_Priority == priority) {
+ if (m_ESPs[i].priority == priority) {
return i;
}
}
@@ -321,8 +330,8 @@ void PluginList::enableSelected(const QItemSelectionModel *selectionModel) bool dirty = false;
for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].m_Enabled) {
- m_ESPs[rowIndex].m_Enabled = true;
+ if (!m_ESPs[rowIndex].enabled) {
+ m_ESPs[rowIndex].enabled = true;
dirty = true;
}
}
@@ -336,8 +345,8 @@ void PluginList::disableSelected(const QItemSelectionModel *selectionModel) bool dirty = false;
for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].m_ForceEnabled && m_ESPs[rowIndex].m_Enabled) {
- m_ESPs[rowIndex].m_Enabled = false;
+ if (!m_ESPs[rowIndex].forceEnabled && m_ESPs[rowIndex].enabled) {
+ m_ESPs[rowIndex].enabled = false;
dirty = true;
}
}
@@ -351,7 +360,7 @@ void PluginList::enableAll() if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all plugins?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
for (ESPInfo &info : m_ESPs) {
- info.m_Enabled = true;
+ info.enabled = true;
}
emit writePluginsList();
}
@@ -363,8 +372,8 @@ void PluginList::disableAll() if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all plugins?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
for (ESPInfo &info : m_ESPs) {
- if (!info.m_ForceEnabled) {
- info.m_Enabled = false;
+ if (!info.forceEnabled) {
+ info.enabled = false;
}
}
emit writePluginsList();
@@ -377,7 +386,7 @@ void PluginList::sendToPriority(const QItemSelectionModel *selectionModel, int n std::vector<int> pluginsToMove;
for (auto row: selectionModel->selectedRows(COL_PRIORITY)) {
int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].m_ForceEnabled) {
+ if (!m_ESPs[rowIndex].forceEnabled) {
pluginsToMove.push_back(rowIndex);
}
}
@@ -392,7 +401,7 @@ bool PluginList::isEnabled(const QString &name) std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- return m_ESPs[iter->second].m_Enabled;
+ return m_ESPs[iter->second].enabled;
} else {
return false;
}
@@ -403,7 +412,7 @@ void PluginList::clearInformation(const QString &name) std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name.toLower()].m_Messages.clear();
+ m_AdditionalInfo[name.toLower()].messages.clear();
}
}
@@ -417,15 +426,26 @@ void PluginList::addInformation(const QString &name, const QString &message) std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name.toLower()].m_Messages.append(message);
+ m_AdditionalInfo[name.toLower()].messages.append(message);
} else {
log::warn("failed to associate message for \"{}\"", name);
}
}
+void PluginList::addLootReport(const QString& name, Loot::Plugin plugin)
+{
+ auto iter = m_ESPsByName.find(name.toLower());
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name.toLower()].loot = std::move(plugin);
+ } else {
+ log::warn("failed to associate loot report for \"{}\"", name);
+ }
+}
+
bool PluginList::isEnabled(int index)
{
- return m_ESPs.at(index).m_Enabled;
+ return m_ESPs.at(index).enabled;
}
void PluginList::readLockedOrderFrom(const QString &fileName)
@@ -450,15 +470,15 @@ void PluginList::readLockedOrderFrom(const QString &fileName) int priority = fields.at(1).trimmed().toInt();
QString name = QString::fromUtf8(fields.at(0));
// Avoid locking a force-enabled plugin
- if (!m_ESPs[m_ESPsByName.at(name)].m_ForceEnabled) {
+ if (!m_ESPs[m_ESPsByName.at(name)].forceEnabled) {
// Is this an open and unclaimed priority?
- if (m_ESPs[m_ESPsByPriority.at(priority)].m_ForceEnabled ||
+ if (m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled ||
std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), [&](const std::pair<QString, int> &a) { return a.second == priority; }) != m_LockedOrder.end()) {
// Attempt to find a priority but step over force-enabled plugins and already-set locks
int calcPriority = priority;
do {
++calcPriority;
- } while (calcPriority < m_ESPsByPriority.size() || (m_ESPs[m_ESPsByPriority.at(calcPriority)].m_ForceEnabled &&
+ } while (calcPriority < m_ESPsByPriority.size() || (m_ESPs[m_ESPsByPriority.at(calcPriority)].forceEnabled &&
std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), [&](const std::pair<QString, int> &a) { return a.second == calcPriority; }) != m_LockedOrder.end()));
// If we have a match, we can reassign the priority...
if (calcPriority < m_ESPsByPriority.size())
@@ -506,8 +526,8 @@ void PluginList::saveTo(const QString &lockedOrderFileName for (size_t i = 0; i < m_ESPs.size(); ++i) {
int priority = m_ESPsByPriority[i];
- if (!m_ESPs[priority].m_Enabled) {
- deleterFile->write(m_ESPs[priority].m_Name.toUtf8());
+ if (!m_ESPs[priority].enabled) {
+ deleterFile->write(m_ESPs[priority].name.toUtf8());
deleterFile->write("\r\n");
}
}
@@ -530,13 +550,16 @@ bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure) log::debug("setting file times on esps");
for (ESPInfo &esp : m_ESPs) {
- std::wstring espName = ToWString(esp.m_Name);
+ std::wstring espName = ToWString(esp.name);
const FileEntry::Ptr fileEntry = directoryStructure.findFile(espName);
if (fileEntry.get() != nullptr) {
QString fileName;
bool archive = false;
int originid = fileEntry->getOrigin(archive);
- fileName = QString("%1\\%2").arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath()))).arg(esp.m_Name);
+
+ fileName = QString("%1\\%2")
+ .arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath())))
+ .arg(esp.name);
HANDLE file = ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
@@ -550,13 +573,13 @@ bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure) }
ULONGLONG temp = 0;
- temp = (145731ULL + esp.m_Priority) * 24 * 60 * 60 * 10000000ULL;
+ temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
FILETIME newWriteTime;
newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
- esp.m_Time = newWriteTime;
+ esp.time = newWriteTime;
fileEntry->setFileTime(newWriteTime);
if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) {
throw windows_error(QObject::tr("failed to set file time %1").arg(fileName).toUtf8().constData());
@@ -572,7 +595,7 @@ int PluginList::enabledCount() const {
int enabled = 0;
for (const auto &info : m_ESPs) {
- if (info.m_Enabled) {
+ if (info.enabled) {
++enabled;
}
}
@@ -581,19 +604,19 @@ int PluginList::enabledCount() const QString PluginList::getIndexPriority(int index) const
{
- return m_ESPs[index].m_Index;
+ return m_ESPs[index].index;
}
bool PluginList::isESPLocked(int index) const
{
- return m_LockedOrder.find(m_ESPs.at(index).m_Name.toLower()) != m_LockedOrder.end();
+ return m_LockedOrder.find(m_ESPs.at(index).name.toLower()) != m_LockedOrder.end();
}
void PluginList::lockESPIndex(int index, bool lock)
{
if (lock) {
- if (!m_ESPs.at(index).m_ForceEnabled)
- m_LockedOrder[getName(index).toLower()] = m_ESPs.at(index).m_LoadOrder;
+ if (!m_ESPs.at(index).forceEnabled)
+ m_LockedOrder[getName(index).toLower()] = m_ESPs.at(index).loadOrder;
else
return;
} else {
@@ -611,10 +634,10 @@ void PluginList::syncLoadOrder() for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
int index = m_ESPsByPriority[i];
- if (m_ESPs[index].m_Enabled) {
- m_ESPs[index].m_LoadOrder = loadOrder++;
+ if (m_ESPs[index].enabled) {
+ m_ESPs[index].loadOrder = loadOrder++;
} else {
- m_ESPs[index].m_LoadOrder = -1;
+ m_ESPs[index].loadOrder = -1;
}
}
}
@@ -639,7 +662,7 @@ void PluginList::refreshLoadOrder() // find the location to insert at
while ((targetPrio < static_cast<int>(m_ESPs.size() - 1)) &&
- (m_ESPs[m_ESPsByPriority[targetPrio]].m_LoadOrder < iter->first)) {
+ (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) {
++targetPrio;
}
@@ -649,9 +672,9 @@ void PluginList::refreshLoadOrder() int temp = targetPrio;
int index = nameIter->second;
- if (m_ESPs[index].m_Priority != temp) {
+ if (m_ESPs[index].priority != temp) {
setPluginPriority(index, temp);
- m_ESPs[index].m_LoadOrder = iter->first;
+ m_ESPs[index].loadOrder = iter->first;
syncLoadOrder();
savePluginsList = true;
}
@@ -678,7 +701,7 @@ QStringList PluginList::pluginNames() const QStringList result;
for (const ESPInfo &info : m_ESPs) {
- result.append(info.m_Name);
+ result.append(info.name);
}
return result;
@@ -690,15 +713,15 @@ IPluginList::PluginStates PluginList::state(const QString &name) const if (iter == m_ESPsByName.end()) {
return IPluginList::STATE_MISSING;
} else {
- return m_ESPs[iter->second].m_Enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
+ return m_ESPs[iter->second].enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
}
}
void PluginList::setState(const QString &name, PluginStates state) {
auto iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].m_Enabled = (state == IPluginList::STATE_ACTIVE) ||
- m_ESPs[iter->second].m_ForceEnabled;
+ m_ESPs[iter->second].enabled = (state == IPluginList::STATE_ACTIVE) ||
+ m_ESPs[iter->second].forceEnabled;
} else {
log::warn("Plugin not found: {}", name);
}
@@ -707,20 +730,20 @@ void PluginList::setState(const QString &name, PluginStates state) { void PluginList::setLoadOrder(const QStringList &pluginList)
{
for (ESPInfo &info : m_ESPs) {
- info.m_Priority = -1;
+ info.priority = -1;
}
int maxPriority = 0;
for (const QString &plugin : pluginList) {
auto iter = m_ESPsByName.find(plugin.toLower());
if (iter !=m_ESPsByName.end()) {
- m_ESPs[iter->second].m_Priority = maxPriority++;
+ m_ESPs[iter->second].priority = maxPriority++;
}
}
// use old priorities
for (ESPInfo &info : m_ESPs) {
- if (info.m_Priority == -1) {
- info.m_Priority = maxPriority++;
+ if (info.priority == -1) {
+ info.priority = maxPriority++;
}
}
updateIndices();
@@ -732,7 +755,7 @@ int PluginList::priority(const QString &name) const if (iter == m_ESPsByName.end()) {
return -1;
} else {
- return m_ESPs[iter->second].m_Priority;
+ return m_ESPs[iter->second].priority;
}
}
@@ -742,7 +765,7 @@ int PluginList::loadOrder(const QString &name) const if (iter == m_ESPsByName.end()) {
return -1;
} else {
- return m_ESPs[iter->second].m_LoadOrder;
+ return m_ESPs[iter->second].loadOrder;
}
}
@@ -752,7 +775,7 @@ bool PluginList::isMaster(const QString &name) const if (iter == m_ESPsByName.end()) {
return false;
} else {
- return m_ESPs[iter->second].m_IsMaster;
+ return m_ESPs[iter->second].isMaster;
}
}
@@ -762,7 +785,7 @@ bool PluginList::isLight(const QString &name) const if (iter == m_ESPsByName.end()) {
return false;
} else {
- return m_ESPs[iter->second].m_IsLight;
+ return m_ESPs[iter->second].isLight;
}
}
@@ -772,7 +795,7 @@ bool PluginList::isLightFlagged(const QString &name) const if (iter == m_ESPsByName.end()) {
return false;
} else {
- return m_ESPs[iter->second].m_IsLightFlagged;
+ return m_ESPs[iter->second].isLightFlagged;
}
}
@@ -783,7 +806,7 @@ QStringList PluginList::masters(const QString &name) const return QStringList();
} else {
QStringList result;
- for (const QString &master : m_ESPs[iter->second].m_Masters) {
+ for (const QString &master : m_ESPs[iter->second].masters) {
result.append(master);
}
return result;
@@ -796,7 +819,7 @@ QString PluginList::origin(const QString &name) const if (iter == m_ESPsByName.end()) {
return QString();
} else {
- return m_ESPs[iter->second].m_OriginName;
+ return m_ESPs[iter->second].originName;
}
}
@@ -826,15 +849,15 @@ void PluginList::updateIndices() m_ESPsByPriority.clear();
m_ESPsByPriority.resize(m_ESPs.size());
for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
- if (m_ESPs[i].m_Priority < 0) {
+ if (m_ESPs[i].priority < 0) {
continue;
}
- if (m_ESPs[i].m_Priority >= static_cast<int>(m_ESPs.size())) {
- log::error("invalid plugin priority: {}", m_ESPs[i].m_Priority);
+ if (m_ESPs[i].priority >= static_cast<int>(m_ESPs.size())) {
+ log::error("invalid plugin priority: {}", m_ESPs[i].priority);
continue;
}
- m_ESPsByName[m_ESPs[i].m_Name.toLower()] = i;
- m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].m_Priority)) = i;
+ m_ESPsByName[m_ESPs[i].name.toLower()] = i;
+ m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].priority)) = i;
}
generatePluginIndexes();
@@ -847,17 +870,17 @@ void PluginList::generatePluginIndexes() bool lightPluginsSupported = m_GamePlugin->feature<GamePlugins>()->lightPluginsAreSupported();
for (int l = 0; l < m_ESPs.size(); ++l) {
int i = m_ESPsByPriority.at(l);
- if (!m_ESPs[i].m_Enabled) {
- m_ESPs[i].m_Index = QString();
+ if (!m_ESPs[i].enabled) {
+ m_ESPs[i].index = QString();
++numSkipped;
continue;
}
- if (lightPluginsSupported && (m_ESPs[i].m_IsLight || m_ESPs[i].m_IsLightFlagged)) {
+ if (lightPluginsSupported && (m_ESPs[i].isLight || m_ESPs[i].isLightFlagged)) {
int ESLpos = 254 + ((numESLs + 1) / 4096);
- m_ESPs[i].m_Index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
+ m_ESPs[i].index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
++numESLs;
} else {
- m_ESPs[i].m_Index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
+ m_ESPs[i].index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
}
}
emit esplist_changed();
@@ -881,160 +904,360 @@ int PluginList::columnCount(const QModelIndex &) const void PluginList::testMasters()
{
-// emit layoutAboutToBeChanged();
-
std::set<QString> enabledMasters;
for (const auto& iter: m_ESPs) {
- if (iter.m_Enabled) {
- enabledMasters.insert(iter.m_Name.toLower());
+ if (iter.enabled) {
+ enabledMasters.insert(iter.name.toLower());
}
}
for (auto& iter: m_ESPs) {
- iter.m_MasterUnset.clear();
- if (iter.m_Enabled) {
- for (const auto& master: iter.m_Masters) {
+ iter.masterUnset.clear();
+ if (iter.enabled) {
+ for (const auto& master: iter.masters) {
if (enabledMasters.find(master.toLower()) == enabledMasters.end()) {
- iter.m_MasterUnset.insert(master);
+ iter.masterUnset.insert(master);
}
}
}
}
-
-#pragma message("emitting this seems to cause a crash!")
-// emit layoutChanged();
}
QVariant PluginList::data(const QModelIndex &modelIndex, int role) const
{
int index = modelIndex.row();
- if ((role == Qt::DisplayRole)
- || (role == Qt::EditRole)) {
- switch (modelIndex.column()) {
- case COL_NAME: {
- return m_ESPs[index].m_Name;
- } break;
- case COL_PRIORITY: {
- return m_ESPs[index].m_Priority;
- } break;
- case COL_MODINDEX: {
- return m_ESPs[index].m_Index;
- } break;
- default: {
- return QVariant();
- } break;
- }
+
+ if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) {
+ return displayData(modelIndex);
} else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) {
- if (m_ESPs[index].m_ForceEnabled) {
- return QVariant();
- } else {
- return m_ESPs[index].m_Enabled ? Qt::Checked : Qt::Unchecked;
- }
+ return checkstateData(modelIndex);
} else if (role == Qt::ForegroundRole) {
- if ((modelIndex.column() == COL_NAME) &&
- m_ESPs[index].m_ForceEnabled) {
- return QBrush(Qt::gray);
- }
- } else if (role == Qt::BackgroundRole
- || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) {
- if (m_ESPs[index].m_ModSelected) {
- return Settings::instance().colors().pluginListContained();
- } else {
- return QVariant();
- }
+ return foregroundData(modelIndex);
+ } else if (role == Qt::BackgroundRole || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) {
+ return backgroundData(modelIndex);
} else if (role == Qt::FontRole) {
- QFont result;
- if (m_ESPs[index].m_IsMaster) {
- result.setItalic(true);
- result.setWeight(QFont::Bold);
- } else if (m_ESPs[index].m_IsLight || m_ESPs[index].m_IsLightFlagged) {
- result.setItalic(true);
- }
- return result;
+ return fontData(modelIndex);
} else if (role == Qt::TextAlignmentRole) {
- if (modelIndex.column() == 0) {
- return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
- } else {
- return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
- }
+ return alignmentData(modelIndex);
} else if (role == Qt::ToolTipRole) {
- QString name = m_ESPs[index].m_Name.toLower();
- auto addInfoIter = m_AdditionalInfo.find(name);
- QString toolTip;
- if (addInfoIter != m_AdditionalInfo.end()) {
- if (!addInfoIter->second.m_Messages.isEmpty()) {
- toolTip += addInfoIter->second.m_Messages.join("<br>") + "<br><hr>";
- }
+ return tooltipData(modelIndex);
+ } else if (role == Qt::UserRole + 1) {
+ return iconData(modelIndex);
+ }
+ return QVariant();
+}
+
+QVariant PluginList::displayData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ switch (modelIndex.column())
+ {
+ case COL_NAME:
+ return m_ESPs[index].name;
+
+ case COL_PRIORITY:
+ return m_ESPs[index].priority;
+
+ case COL_MODINDEX:
+ return m_ESPs[index].index;
+
+ default:
+ return {};
+ }
+}
+
+QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].forceEnabled) {
+ return {};
+ }
+
+ return m_ESPs[index].enabled ? Qt::Checked : Qt::Unchecked;
+}
+
+QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceEnabled) {
+ return QBrush(Qt::gray);
+ }
+
+ return {};
+}
+
+QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].modSelected) {
+ return Settings::instance().colors().pluginListContained();
+ }
+
+ return {};
+}
+
+QVariant PluginList::fontData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ QFont result;
+
+ if (m_ESPs[index].isMaster) {
+ result.setItalic(true);
+ result.setWeight(QFont::Bold);
+ } else if (m_ESPs[index].isLight || m_ESPs[index].isLightFlagged) {
+ result.setItalic(true);
+ }
+
+ return result;
+}
+
+QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (modelIndex.column() == 0) {
+ return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+ } else {
+ return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
+ }
+}
+
+QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+ const auto& esp = m_ESPs[index];
+
+ QString toolTip;
+
+ toolTip += "<b>" + tr("Origin") + "</b>: " + esp.originName;
+
+ if (esp.forceEnabled) {
+ toolTip +=
+ "<br><b><i>" +
+ tr("This plugin can't be disabled (enforced by the game).") +
+ "</i></b>";
+ } else {
+ if (!esp.author.isEmpty()) {
+ toolTip +=
+ "<br><b>" + tr("Author") + "</b>: " +
+ TruncateString(esp.author);
}
- if (m_ESPs[index].m_ForceEnabled) {
- QString text = tr("<b>Origin</b>: %1").arg(m_ESPs[index].m_OriginName);
- text += tr("<br><b><i>This plugin can't be disabled (enforced by the game).</i></b>");
- toolTip += text;
- } else {
- QString text = tr("<b>Origin</b>: %1").arg(m_ESPs[index].m_OriginName);
- if (m_ESPs[index].m_Author.size() > 0) {
- text += "<br><b>" + tr("Author") + "</b>: " + TruncateString(m_ESPs[index].m_Author);
- }
- if (m_ESPs[index].m_Description.size() > 0) {
- text += "<br><b>" + tr("Description") + "</b>: " + TruncateString(m_ESPs[index].m_Description);
- }
- if (m_ESPs[index].m_MasterUnset.size() > 0) {
- text += "<br><b>" + tr("Missing Masters") + "</b>: <b>" + TruncateString(SetJoin(m_ESPs[index].m_MasterUnset, ", ")) + "</b>";
- }
- std::set<QString> enabledMasters;
- std::set_difference(m_ESPs[index].m_Masters.begin(), m_ESPs[index].m_Masters.end(),
- m_ESPs[index].m_MasterUnset.begin(), m_ESPs[index].m_MasterUnset.end(),
- std::inserter(enabledMasters, enabledMasters.end()));
- if (!enabledMasters.empty()) {
- text += "<br><b>" + tr("Enabled Masters") + "</b>: " + TruncateString(SetJoin(enabledMasters, ", "));
- }
- if (!m_ESPs[index].m_Archives.empty()) {
- text += "<br><b>" + tr("Loads Archives") + "</b>: " + TruncateString(SetJoin(m_ESPs[index].m_Archives, ", "));
- text += "<br>" + tr("There are Archives connected to this plugin. "
- "Their assets will be added to your game, overwriting in case of conflicts following the plugin order. "
- "Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)");
- }
- if (m_ESPs[index].m_HasIni) {
- text += "<br><b>" + tr("Loads INI settings") + "</b>: ";
- text += "<br>" + tr("There is an ini file connected to this plugin. "
- "Its settings will be added to your game settings, overwriting in case of conflicts.");
- }
- if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) {
- text += "<br><br>" + tr("This ESP is flagged as an ESL. "
- "It will adhere to the ESP load order but the records will be loaded in ESL space.");
- }
- toolTip += text;
+
+ if (esp.description.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Description") + "</b>: " +
+ TruncateString(esp.description);
}
- return toolTip;
- } else if (role == Qt::UserRole + 1) {
- QVariantList result;
- QString nameLower = m_ESPs[index].m_Name.toLower();
- if (m_ESPs[index].m_MasterUnset.size() > 0) {
- result.append(":/MO/gui/warning");
+
+ if (esp.masterUnset.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Missing Masters") + "</b>: " +
+ "<b>" + TruncateString(SetJoin(esp.masterUnset, ", ")) + "</b>";
+ }
+
+ std::set<QString> enabledMasters;
+ std::set_difference(esp.masters.begin(), esp.masters.end(),
+ esp.masterUnset.begin(), esp.masterUnset.end(),
+ std::inserter(enabledMasters, enabledMasters.end()));
+
+ if (!enabledMasters.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Enabled Masters") + "</b>: " +
+ TruncateString(SetJoin(enabledMasters, ", "));
+ }
+
+ if (!esp.archives.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Loads Archives") + "</b>: " +
+ TruncateString(SetJoin(esp.archives, ", ")) +
+ "<br>" + tr(
+ "There are Archives connected to this plugin. Their assets will be "
+ "added to your game, overwriting in case of conflicts following the "
+ "plugin order. Loose files will always overwrite assets from "
+ "Archives. (This flag only checks for Archives from the same mod as "
+ "the plugin)");
}
- if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) {
- result.append(":/MO/gui/locked");
+
+ if (esp.hasIni) {
+ toolTip +=
+ "<br><b>" + tr("Loads INI settings") + "</b>: "
+ "<br>" + tr(
+ "There is an ini file connected to this plugin. Its settings will "
+ "be added to your game settings, overwriting in case of conflicts.");
+ }
+
+ if (esp.isLightFlagged && !esp.isLight) {
+ toolTip +=
+ "<br><br>" + tr(
+ "This ESP is flagged as an ESL. It will adhere to the ESP load "
+ "order but the records will be loaded in ESL space.");
}
- auto bossInfoIter = m_AdditionalInfo.find(nameLower);
- if (bossInfoIter != m_AdditionalInfo.end()) {
- if (!bossInfoIter->second.m_Messages.isEmpty()) {
- result.append(":/MO/gui/information");
+ }
+
+
+ // additional info
+ auto itor = m_AdditionalInfo.find(esp.name.toLower());
+
+ if (itor != m_AdditionalInfo.end()) {
+ if (!itor->second.messages.isEmpty()) {
+ toolTip += "<hr><ul style=\"margin-left:15px; -qt-list-indent: 0;\">";
+
+ for (auto&& message : itor->second.messages) {
+ toolTip += "<li>" + message + "</li>";
}
+
+ toolTip += "</ul>";
}
- if (m_ESPs[index].m_HasIni) {
- result.append(":/MO/gui/attachment");
+
+ // loot
+ toolTip += makeLootTooltip(itor->second.loot);
+ }
+
+ return toolTip;
+}
+
+QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
+{
+ QString s;
+
+ for (auto&& f : loot.incompatibilities) {
+ s +=
+ "<li>" + tr("Incompatible with %1")
+ .arg(f.displayName.isEmpty() ? f.name : f.displayName) +
+ "</li>";
+ }
+
+ for (auto&& m : loot.missingMasters) {
+ s += "<li>" + tr("Depends on missing %1").arg(m) + "</li>";
+ }
+
+ for (auto&& m : loot.messages) {
+ s += "<li>";
+
+ switch (m.type)
+ {
+ case log::Warning:
+ s += tr("Warning") + ": ";
+ break;
+
+ case log::Error:
+ s += tr("Error") + ": ";
+ break;
+
+ case log::Info: // fall-through
+ case log::Debug:
+ default:
+ // nothing
+ break;
}
- if (!m_ESPs[index].m_Archives.empty()) {
- result.append(":/MO/gui/archive_conflict_neutral");
+
+ s += m.text + "</li>";
+ }
+
+ for (auto&& d : loot.dirty) {
+ s += "<li>" + d.toString(false) + "</li>";
+ }
+
+ for (auto&& c : loot.clean) {
+ s += "<li>" + c.toString(true) + "</li>";
+ }
+
+ if (!s.isEmpty()) {
+ s =
+ "<hr>"
+ "<ul style=\"margin-top:0px; padding-top:0px; margin-left:15px; -qt-list-indent: 0;\">" +
+ s +
+ "</ul>";
+ }
+
+ return s;
+}
+
+QVariant PluginList::iconData(const QModelIndex &modelIndex) const
+{
+ int index = modelIndex.row();
+
+ QVariantList result;
+
+ const auto& esp = m_ESPs[index];
+ const QString nameLower = esp.name.toLower();
+
+ auto infoItor = m_AdditionalInfo.find(nameLower);
+
+ const AdditionalInfo* info = nullptr;
+ if (infoItor != m_AdditionalInfo.end()) {
+ info = &infoItor->second;
+ }
+
+ if (isProblematic(esp, info)) {
+ result.append(":/MO/gui/warning");
+ }
+
+ if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) {
+ result.append(":/MO/gui/locked");
+ }
+
+ if (hasInfo(esp, info)) {
+ result.append(":/MO/gui/information");
+ }
+
+ if (esp.hasIni) {
+ result.append(":/MO/gui/attachment");
+ }
+
+ if (!esp.archives.empty()) {
+ result.append(":/MO/gui/archive_conflict_neutral");
+ }
+
+ if (esp.isLightFlagged && !m_ESPs[index].isLight) {
+ result.append(":/MO/gui/awaiting");
+ }
+
+ return result;
+}
+
+bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (esp.masterUnset.size() > 0) {
+ return true;
+ }
+
+ if (info) {
+ if (!info->loot.incompatibilities.empty()) {
+ return true;
}
- if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) {
- result.append(":/MO/gui/awaiting");
+
+ if (!info->loot.missingMasters.empty()) {
+ return true;
}
- return result;
}
- return QVariant();
+
+ return false;
}
+bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (info) {
+ if (!info->messages.empty()) {
+ return true;
+ }
+
+ if (!info->loot.messages.empty()) {
+ return true;
+ }
+
+ if (!info->loot.dirty.empty()) {
+ return true;
+ }
+ }
+
+ return false;
+}
bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role)
{
@@ -1044,8 +1267,8 @@ bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int bool result = false;
if (role == Qt::CheckStateRole) {
- m_ESPs[modIndex.row()].m_Enabled =
- value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].m_ForceEnabled;
+ m_ESPs[modIndex.row()].enabled =
+ value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].forceEnabled;
m_LastCheck.restart();
emit dataChanged(modIndex, modIndex);
@@ -1109,7 +1332,7 @@ Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
if (modelIndex.isValid()) {
- if (!m_ESPs[index].m_ForceEnabled) {
+ if (!m_ESPs[index].forceEnabled) {
result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
}
if (modelIndex.column() == COL_PRIORITY) {
@@ -1134,48 +1357,48 @@ void PluginList::setPluginPriority(int row, int &newPriority) else if (newPriorityTemp >= static_cast<int>(m_ESPsByPriority.size()))
newPriorityTemp = static_cast<int>(m_ESPsByPriority.size()) - 1;
- if (!m_ESPs[row].m_IsMaster && !m_ESPs[row].m_IsLight) {
+ if (!m_ESPs[row].isMaster && !m_ESPs[row].isLight) {
// don't allow esps to be moved above esms
while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsMaster ||
- m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsLight)) {
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMaster ||
+ m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isLight)) {
++newPriorityTemp;
}
} else {
// don't allow esms to be moved below esps
while ((newPriorityTemp > 0) &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsMaster &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsLight) {
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMaster &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isLight) {
--newPriorityTemp;
}
// also don't allow "regular" esms to be moved above primary plugins
while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_ForceEnabled)) {
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceEnabled)) {
++newPriorityTemp;
}
}
try {
- int oldPriority = m_ESPs.at(row).m_Priority;
+ int oldPriority = m_ESPs.at(row).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_ESPs.at(m_ESPsByPriority.at(i)).m_Priority;
+ --m_ESPs.at(m_ESPsByPriority.at(i)).priority;
}
emit dataChanged(index(oldPriority + 1, 0), index(newPriorityTemp, columnCount()));
} else {
for (int i = newPriorityTemp; i < oldPriority; ++i) {
- ++m_ESPs.at(m_ESPsByPriority.at(i)).m_Priority;
+ ++m_ESPs.at(m_ESPsByPriority.at(i)).priority;
}
emit dataChanged(index(newPriorityTemp, 0), index(oldPriority - 1, columnCount()));
++newPriority;
}
- m_ESPs.at(row).m_Priority = newPriorityTemp;
+ m_ESPs.at(row).priority = newPriorityTemp;
emit dataChanged(index(row, 0), index(row, columnCount()));
- m_PluginMoved(m_ESPs[row].m_Name, oldPriority, newPriorityTemp);
+ m_PluginMoved(m_ESPs[row].name, oldPriority, newPriorityTemp);
} catch (const std::out_of_range&) {
- reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].m_Name));
+ reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].name));
}
updateIndices();
@@ -1193,11 +1416,11 @@ void PluginList::changePluginPriority(std::vector<int> rows, int newPriority) // don't try to move plugins before force-enabled plugins
for (std::vector<ESPInfo>::const_iterator iter = m_ESPs.begin();
iter != m_ESPs.end(); ++iter) {
- if (iter->m_ForceEnabled) {
- newPriority = std::max(newPriority, iter->m_Priority+1);
+ if (iter->forceEnabled) {
+ newPriority = std::max(newPriority, iter->priority+1);
}
- maxPriority = std::max(maxPriority, iter->m_Priority+1);
- minPriority = std::min(minPriority, iter->m_Priority);
+ maxPriority = std::max(maxPriority, iter->priority+1);
+ minPriority = std::min(minPriority, iter->priority);
}
// limit the new priority to existing priorities
@@ -1207,14 +1430,14 @@ void PluginList::changePluginPriority(std::vector<int> rows, int newPriority) // sort the moving plugins by ascending priorities
std::sort(rows.begin(), rows.end(),
[&esp](const int &LHS, const int &RHS) {
- return esp[LHS].m_Priority < esp[RHS].m_Priority;
+ return esp[LHS].priority < esp[RHS].priority;
});
// if at least on plugin is increasing in priority, the target index is
// that of the row BELOW the dropped location, otherwise it's the one above
for (std::vector<int>::const_iterator iter = rows.begin();
iter != rows.end(); ++iter) {
- if (m_ESPs[*iter].m_Priority < newPriority) {
+ if (m_ESPs[*iter].priority < newPriority) {
--newPriority;
break;
}
@@ -1259,7 +1482,7 @@ bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, (row >= static_cast<int>(m_ESPs.size()))) {
newPriority = static_cast<int>(m_ESPs.size());
} else {
- newPriority = m_ESPs[row].m_Priority;
+ newPriority = m_ESPs[row].priority;
}
changePluginPriority(sourceRows, newPriority);
@@ -1316,7 +1539,7 @@ bool PluginList::eventFilter(QObject *obj, QEvent *event) }
for (QModelIndex idx : rows) {
idx = proxyModel->mapToSource(idx);
- int newPriority = m_ESPs[idx.row()].m_Priority + diff;
+ int newPriority = m_ESPs[idx.row()].priority + diff;
if ((newPriority >= 0) && (newPriority < rowCount())) {
setPluginPriority(idx.row(), newPriority);
}
@@ -1358,27 +1581,28 @@ bool PluginList::eventFilter(QObject *obj, QEvent *event) PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
const QString &originName, const QString &fullPath,
bool hasIni, std::set<QString> archives, bool lightPluginsAreSupported)
- : m_Name(name), m_FullPath(fullPath), m_Enabled(enabled), m_ForceEnabled(enabled),
- m_Priority(0), m_LoadOrder(-1), m_OriginName(originName), m_HasIni(hasIni), m_Archives(archives), m_ModSelected(false)
+ : name(name), fullPath(fullPath), enabled(enabled), forceEnabled(enabled),
+ priority(0), loadOrder(-1), originName(originName), hasIni(hasIni),
+ archives(archives), modSelected(false)
{
try {
ESP::File file(ToWString(fullPath));
- m_IsMaster = file.isMaster();
+ isMaster = file.isMaster();
auto extension = name.right(3).toLower();
- m_IsLight = lightPluginsAreSupported && (extension == "esl");
- m_IsLightFlagged = lightPluginsAreSupported && file.isLight();
+ isLight = lightPluginsAreSupported && (extension == "esl");
+ isLightFlagged = lightPluginsAreSupported && file.isLight();
+
+ author = QString::fromLatin1(file.author().c_str());
+ description = QString::fromLatin1(file.description().c_str());
- m_Author = QString::fromLatin1(file.author().c_str());
- m_Description = QString::fromLatin1(file.description().c_str());
- std::set<std::string> masters = file.masters();
- for (auto iter = masters.begin(); iter != masters.end(); ++iter) {
- m_Masters.insert(QString(iter->c_str()));
+ for (auto&& m : file.masters()) {
+ masters.insert(QString::fromStdString(m));
}
} catch (const std::exception &e) {
log::error("failed to parse plugin file {}: {}", fullPath, e.what());
- m_IsMaster = false;
- m_IsLight = false;
- m_IsLightFlagged = false;
+ isMaster = false;
+ isLight = false;
+ isLightFlagged = false;
}
}
diff --git a/src/pluginlist.h b/src/pluginlist.h index 092ba378..004b1590 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -23,6 +23,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <directoryentry.h>
#include <ipluginlist.h>
#include "profile.h"
+#include "loot.h"
+
namespace MOBase { class IPluginGame; }
#include <QString>
@@ -155,6 +157,11 @@ public: void addInformation(const QString &name, const QString &message);
/**
+ * adds information from a loot report
+ */
+ void addLootReport(const QString& name, Loot::Plugin plugin);
+
+ /**
* @brief test if a plugin is enabled
*
* @param index index of the plugin to look up
@@ -195,8 +202,8 @@ public: int timeElapsedSinceLastChecked() const;
- QString getName(int index) const { return m_ESPs.at(index).m_Name; }
- int getPriority(int index) const { return m_ESPs.at(index).m_Priority; }
+ QString getName(int index) const { return m_ESPs.at(index).name; }
+ int getPriority(int index) const { return m_ESPs.at(index).priority; }
QString getIndexPriority(int index) const;
bool isESPLocked(int index) const;
void lockESPIndex(int index, bool lock);
@@ -294,36 +301,42 @@ signals: private:
- struct ESPInfo {
+ struct ESPInfo
+ {
+ ESPInfo(
+ const QString &name, bool enabled, const QString &originName,
+ const QString &fullPath, bool hasIni, std::set<QString> archives,
+ bool lightSupported);
+
+ QString name;
+ QString fullPath;
+ bool enabled;
+ bool forceEnabled;
+ int priority;
+ QString index;
+ int loadOrder;
+ FILETIME time;
+ QString originName;
+ bool isMaster;
+ bool isLight;
+ bool isLightFlagged;
+ bool modSelected;
+ QString author;
+ QString description;
+ bool hasIni;
+ std::set<QString> archives;
+ std::set<QString> masters;
+ mutable std::set<QString> masterUnset;
- ESPInfo(const QString &name, bool enabled, const QString &originName, const QString &fullPath, bool hasIni, std::set<QString> archives, bool lightSupported);
- QString m_Name;
- QString m_FullPath;
- bool m_Enabled;
- bool m_ForceEnabled;
- int m_Priority;
- QString m_Index;
- int m_LoadOrder;
- FILETIME m_Time;
- QString m_OriginName;
- bool m_IsMaster;
- bool m_IsLight;
- bool m_IsLightFlagged;
- bool m_ModSelected;
- QString m_Author;
- QString m_Description;
- bool m_HasIni;
- std::set<QString> m_Archives;
- std::set<QString> m_Masters;
- mutable std::set<QString> m_MasterUnset;
bool operator < (const ESPInfo& str) const
{
- return (m_LoadOrder < str.m_LoadOrder);
+ return (loadOrder < str.loadOrder);
}
};
struct AdditionalInfo {
- QStringList m_Messages;
+ QStringList messages;
+ Loot::Plugin loot;
};
friend bool ByName(const ESPInfo& LHS, const ESPInfo& RHS);
@@ -372,6 +385,19 @@ private: const MOBase::IPluginGame *m_GamePlugin;
+
+ QVariant displayData(const QModelIndex &modelIndex) const;
+ QVariant checkstateData(const QModelIndex &modelIndex) const;
+ QVariant foregroundData(const QModelIndex &modelIndex) const;
+ QVariant backgroundData(const QModelIndex &modelIndex) const;
+ QVariant fontData(const QModelIndex &modelIndex) const;
+ QVariant alignmentData(const QModelIndex &modelIndex) const;
+ QVariant tooltipData(const QModelIndex &modelIndex) const;
+ QVariant iconData(const QModelIndex &modelIndex) const;
+
+ QString makeLootTooltip(const Loot::Plugin& loot) const;
+ bool isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const;
+ bool hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const;
};
#pragma warning(pop)
diff --git a/src/resources/markdown.html b/src/resources/markdown.html new file mode 100644 index 00000000..bd6d0f4d --- /dev/null +++ b/src/resources/markdown.html @@ -0,0 +1,292 @@ +<!doctype html> +<html lang="en"> +<meta charset="utf-8"> +<head> + <script> + /** + * marked - a markdown parser + * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + !function(e){"use strict";var x={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:/^ {0,3}(`{3,}|~{3,})([^`~\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6}) +([^\n]*?)(?: +#+)? *(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|<![A-Z][\\s\\S]*?>\\n*|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>\\n*|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|</(?!script|pre|style)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *<?([^\s>]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:g,table:g,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html)[^\n]+)*)/,text:/^[^\n]+/};function a(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||k.defaults,this.rules=x.normal,this.options.pedantic?this.rules=x.pedantic:this.options.gfm&&(this.rules=x.gfm)}x._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,x._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,x.def=i(x.def).replace("label",x._label).replace("title",x._title).getRegex(),x.bullet=/(?:[*+-]|\d{1,9}\.)/,x.item=/^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/,x.item=i(x.item,"gm").replace(/bull/g,x.bullet).getRegex(),x.list=i(x.list).replace(/bull/g,x.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+x.def.source+")").getRegex(),x._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",x._comment=/<!--(?!-?>)[\s\S]*?-->/,x.html=i(x.html,"i").replace("comment",x._comment).replace("tag",x._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),x.paragraph=i(x._paragraph).replace("hr",x.hr).replace("heading"," {0,3}#{1,6} +").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}|~{3,})[^`\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|!--)").replace("tag",x._tag).getRegex(),x.blockquote=i(x.blockquote).replace("paragraph",x.paragraph).getRegex(),x.normal=f({},x),x.gfm=f({},x.normal,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),x.pedantic=f({},x.normal,{html:i("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:\"[^\"]*\"|'[^']*'|\\s[^'\"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",x._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,fences:g,paragraph:i(x.normal._paragraph).replace("hr",x.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",x.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),a.rules=x,a.lex=function(e,t){return new a(t).lex(e)},a.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},a.prototype.token=function(e,t){var n,r,s,i,l,o,a,h,p,u,c,g,f,d,m,k;for(e=e.replace(/^ +$/gm,"");e;)if((s=this.rules.newline.exec(e))&&(e=e.substring(s[0].length),1<s[0].length&&this.tokens.push({type:"space"})),s=this.rules.code.exec(e)){var b=this.tokens[this.tokens.length-1];e=e.substring(s[0].length),b&&"paragraph"===b.type?b.text+="\n"+s[0].trimRight():(s=s[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",codeBlockStyle:"indented",text:this.options.pedantic?s:w(s,"\n")}))}else if(s=this.rules.fences.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"code",lang:s[2]?s[2].trim():s[2],text:s[3]||""});else if(s=this.rules.heading.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"heading",depth:s[1].length,text:s[2]});else if((s=this.rules.nptable.exec(e))&&(o={type:"table",header:y(s[1].replace(/^ *| *\| *$/g,"")),align:s[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:s[3]?s[3].replace(/\n$/,"").split("\n"):[]}).header.length===o.align.length){for(e=e.substring(s[0].length),c=0;c<o.align.length;c++)/^ *-+: *$/.test(o.align[c])?o.align[c]="right":/^ *:-+: *$/.test(o.align[c])?o.align[c]="center":/^ *:-+ *$/.test(o.align[c])?o.align[c]="left":o.align[c]=null;for(c=0;c<o.cells.length;c++)o.cells[c]=y(o.cells[c],o.header.length);this.tokens.push(o)}else if(s=this.rules.hr.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"hr"});else if(s=this.rules.blockquote.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"blockquote_start"}),s=s[0].replace(/^ *> ?/gm,""),this.token(s,t),this.tokens.push({type:"blockquote_end"});else if(s=this.rules.list.exec(e)){for(e=e.substring(s[0].length),a={type:"list_start",ordered:d=1<(i=s[2]).length,start:d?+i:"",loose:!1},this.tokens.push(a),n=!(h=[]),f=(s=s[0].match(this.rules.item)).length,c=0;c<f;c++)u=(o=s[c]).length,~(o=o.replace(/^ *([*+-]|\d+\.) */,"")).indexOf("\n ")&&(u-=o.length,o=this.options.pedantic?o.replace(/^ {1,4}/gm,""):o.replace(new RegExp("^ {1,"+u+"}","gm"),"")),c!==f-1&&(l=x.bullet.exec(s[c+1])[0],(1<i.length?1===l.length:1<l.length||this.options.smartLists&&l!==i)&&(e=s.slice(c+1).join("\n")+e,c=f-1)),r=n||/\n\n(?!\s*$)/.test(o),c!==f-1&&(n="\n"===o.charAt(o.length-1),r||(r=n)),r&&(a.loose=!0),k=void 0,(m=/^\[[ xX]\] /.test(o))&&(k=" "!==o[1],o=o.replace(/^\[[ xX]\] +/,"")),p={type:"list_item_start",task:m,checked:k,loose:r},h.push(p),this.tokens.push(p),this.token(o,!1),this.tokens.push({type:"list_item_end"});if(a.loose)for(f=h.length,c=0;c<f;c++)h[c].loose=!0;this.tokens.push({type:"list_end"})}else if(s=this.rules.html.exec(e))e=e.substring(s[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===s[1]||"script"===s[1]||"style"===s[1]),text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(s[0]):_(s[0]):s[0]});else if(t&&(s=this.rules.def.exec(e)))e=e.substring(s[0].length),s[3]&&(s[3]=s[3].substring(1,s[3].length-1)),g=s[1].toLowerCase().replace(/\s+/g," "),this.tokens.links[g]||(this.tokens.links[g]={href:s[2],title:s[3]});else if((s=this.rules.table.exec(e))&&(o={type:"table",header:y(s[1].replace(/^ *| *\| *$/g,"")),align:s[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:s[3]?s[3].replace(/\n$/,"").split("\n"):[]}).header.length===o.align.length){for(e=e.substring(s[0].length),c=0;c<o.align.length;c++)/^ *-+: *$/.test(o.align[c])?o.align[c]="right":/^ *:-+: *$/.test(o.align[c])?o.align[c]="center":/^ *:-+ *$/.test(o.align[c])?o.align[c]="left":o.align[c]=null;for(c=0;c<o.cells.length;c++)o.cells[c]=y(o.cells[c].replace(/^ *\| *| *\| *$/g,""),o.header.length);this.tokens.push(o)}else if(s=this.rules.lheading.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"heading",depth:"="===s[2].charAt(0)?1:2,text:s[1]});else if(t&&(s=this.rules.paragraph.exec(e)))e=e.substring(s[0].length),this.tokens.push({type:"paragraph",text:"\n"===s[1].charAt(s[1].length-1)?s[1].slice(0,-1):s[1]});else if(s=this.rules.text.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"text",text:s[0]});else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0));return this.tokens};var n={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:g,tag:"^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*<\[])\*(?!\*)|^_([^\s<][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_<][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s<"][\s\S]*?[^\s\*])\*(?!\*|[^\spunctuation])|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:g,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*]|\b_|$)|[^ ](?= {2,}\n))|(?= {2,}\n))/};function p(e,t){if(this.options=t||k.defaults,this.links=e,this.rules=n.normal,this.renderer=this.options.renderer||new r,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.pedantic?this.rules=n.pedantic:this.options.gfm&&(this.options.breaks?this.rules=n.breaks:this.rules=n.gfm)}function r(e){this.options=e||k.defaults}function s(){}function h(e){this.tokens=[],this.token=null,this.options=e||k.defaults,this.options.renderer=this.options.renderer||new r,this.renderer=this.options.renderer,this.renderer.options=this.options,this.slugger=new t}function t(){this.seen={}}function _(e,t){if(t){if(_.escapeTest.test(e))return e.replace(_.escapeReplace,function(e){return _.replacements[e]})}else if(_.escapeTestNoEncode.test(e))return e.replace(_.escapeReplaceNoEncode,function(e){return _.replacements[e]});return e}function c(e){return e.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function i(n,e){return n=n.source||n,e=e||"",{replace:function(e,t){return t=(t=t.source||t).replace(/(^|[^\[])\^/g,"$1"),n=n.replace(e,t),this},getRegex:function(){return new RegExp(n,e)}}}function l(e,t,n){if(e){try{var r=decodeURIComponent(c(n)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return null}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:")||0===r.indexOf("data:"))return null}t&&!u.test(n)&&(n=function(e,t){o[" "+e]||(/^[^:]+:\/*[^/]*$/.test(e)?o[" "+e]=e+"/":o[" "+e]=w(e,"/",!0));return e=o[" "+e],"//"===t.slice(0,2)?e.replace(/:[\s\S]*/,":")+t:"/"===t.charAt(0)?e.replace(/(:\/*[^/]*)[\s\S]*/,"$1")+t:e+t}(t,n));try{n=encodeURI(n).replace(/%25/g,"%")}catch(e){return null}return n}n._punctuation="!\"#$%&'()*+,\\-./:;<=>?@\\[^_{|}~",n.em=i(n.em).replace(/punctuation/g,n._punctuation).getRegex(),n._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=i(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=i(n.tag).replace("comment",x._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,n._href=/<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=i(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=i(n.reflink).replace("label",n._label).getRegex(),n.normal=f({},n),n.pedantic=f({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:i(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:i(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=f({},n.normal,{escape:i(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*~]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))|(?= {2,}\n|[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))/}),n.gfm.url=i(n.gfm.url,"i").replace("email",n.gfm._extended_email).getRegex(),n.breaks=f({},n.gfm,{br:i(n.br).replace("{2,}","*").getRegex(),text:i(n.gfm.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()}),p.rules=n,p.output=function(e,t,n){return new p(t,n).output(e)},p.prototype.output=function(e){for(var t,n,r,s,i,l,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=_(i[1]);else if(i=this.rules.tag.exec(e))!this.inLink&&/^<a /i.test(i[0])?this.inLink=!0:this.inLink&&/^<\/a>/i.test(i[0])&&(this.inLink=!1),!this.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(i[0])?this.inRawBlock=!0:this.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(i[0])&&(this.inRawBlock=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):_(i[0]):i[0];else if(i=this.rules.link.exec(e)){var a=d(i[2],"()");if(-1<a){var h=4+i[1].length+a;i[2]=i[2].substring(0,a),i[0]=i[0].substring(0,h).trim(),i[3]=""}e=e.substring(i[0].length),this.inLink=!0,r=i[2],s=this.options.pedantic?(t=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r))?(r=t[1],t[3]):"":i[3]?i[3].slice(1,-1):"",r=r.trim().replace(/^<([\s\S]*)>$/,"$1"),o+=this.outputLink(i,{href:p.escapes(r),title:p.escapes(s)}),this.inLink=!1}else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[4]||i[3]||i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[6]||i[5]||i[4]||i[3]||i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(_(i[2].trim(),!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),r="@"===i[2]?"mailto:"+(n=_(this.mangle(i[1]))):n=_(i[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.text.exec(e))e=e.substring(i[0].length),this.inRawBlock?o+=this.renderer.text(this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):_(i[0]):i[0]):o+=this.renderer.text(_(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===i[2])r="mailto:"+(n=_(i[0]));else{for(;l=i[0],i[0]=this.rules._backpedal.exec(i[0])[0],l!==i[0];);n=_(i[0]),r="www."===i[1]?"http://"+n:n}e=e.substring(i[0].length),o+=this.renderer.link(r,null,n)}return o},p.escapes=function(e){return e?e.replace(p.rules._escapes,"$1"):e},p.prototype.outputLink=function(e,t){var n=t.href,r=t.title?_(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,_(e[1]))},p.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},p.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,s=0;s<r;s++)t=e.charCodeAt(s),.5<Math.random()&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},r.prototype.code=function(e,t,n){var r=(t||"").match(/\S*/)[0];if(this.options.highlight){var s=this.options.highlight(e,r);null!=s&&s!==e&&(n=!0,e=s)}return r?'<pre><code class="'+this.options.langPrefix+_(r,!0)+'">'+(n?e:_(e,!0))+"</code></pre>\n":"<pre><code>"+(n?e:_(e,!0))+"</code></pre>"},r.prototype.blockquote=function(e){return"<blockquote>\n"+e+"</blockquote>\n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n,r){return this.options.headerIds?"<h"+t+' id="'+this.options.headerPrefix+r.slug(n)+'">'+e+"</h"+t+">\n":"<h"+t+">"+e+"</h"+t+">\n"},r.prototype.hr=function(){return this.options.xhtml?"<hr/>\n":"<hr>\n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"</"+r+">\n"},r.prototype.listitem=function(e){return"<li>"+e+"</li>\n"},r.prototype.checkbox=function(e){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox"'+(this.options.xhtml?" /":"")+"> "},r.prototype.paragraph=function(e){return"<p>"+e+"</p>\n"},r.prototype.table=function(e,t){return t&&(t="<tbody>"+t+"</tbody>"),"<table>\n<thead>\n"+e+"</thead>\n"+t+"</table>\n"},r.prototype.tablerow=function(e){return"<tr>\n"+e+"</tr>\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"</"+n+">\n"},r.prototype.strong=function(e){return"<strong>"+e+"</strong>"},r.prototype.em=function(e){return"<em>"+e+"</em>"},r.prototype.codespan=function(e){return"<code>"+e+"</code>"},r.prototype.br=function(){return this.options.xhtml?"<br/>":"<br>"},r.prototype.del=function(e){return"<del>"+e+"</del>"},r.prototype.link=function(e,t,n){if(null===(e=l(this.options.sanitize,this.options.baseUrl,e)))return n;var r='<a href="'+_(e)+'"';return t&&(r+=' title="'+t+'"'),r+=">"+n+"</a>"},r.prototype.image=function(e,t,n){if(null===(e=l(this.options.sanitize,this.options.baseUrl,e)))return n;var r='<img src="'+e+'" alt="'+n+'"';return t&&(r+=' title="'+t+'"'),r+=this.options.xhtml?"/>":">"},r.prototype.text=function(e){return e},s.prototype.strong=s.prototype.em=s.prototype.codespan=s.prototype.del=s.prototype.text=function(e){return e},s.prototype.link=s.prototype.image=function(e,t,n){return""+n},s.prototype.br=function(){return""},h.parse=function(e,t){return new h(t).parse(e)},h.prototype.parse=function(e){this.inline=new p(e.links,this.options),this.inlineText=new p(e.links,f({},this.options,{renderer:new s})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},h.prototype.next=function(){return this.token=this.tokens.pop(),this.token},h.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},h.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},h.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,c(this.inlineText.output(this.token.text)),this.slugger);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s="",i="";for(n="",e=0;e<this.token.header.length;e++)n+=this.renderer.tablecell(this.inline.output(this.token.header[e]),{header:!0,align:this.token.align[e]});for(s+=this.renderer.tablerow(n),e=0;e<this.token.cells.length;e++){for(t=this.token.cells[e],n="",r=0;r<t.length;r++)n+=this.renderer.tablecell(this.inline.output(t[r]),{header:!1,align:this.token.align[r]});i+=this.renderer.tablerow(n)}return this.renderer.table(s,i);case"blockquote_start":for(i="";"blockquote_end"!==this.next().type;)i+=this.tok();return this.renderer.blockquote(i);case"list_start":i="";for(var l=this.token.ordered,o=this.token.start;"list_end"!==this.next().type;)i+=this.tok();return this.renderer.list(i,l,o);case"list_item_start":i="";var a=this.token.loose,h=this.token.checked,p=this.token.task;for(this.token.task&&(i+=this.renderer.checkbox(h));"list_item_end"!==this.next().type;)i+=a||"text"!==this.token.type?this.tok():this.parseText();return this.renderer.listitem(i,p,h);case"html":return this.renderer.html(this.token.text);case"paragraph":return this.renderer.paragraph(this.inline.output(this.token.text));case"text":return this.renderer.paragraph(this.parseText());default:var u='Token with "'+this.token.type+'" type was not found.';if(!this.options.silent)throw new Error(u);console.log(u)}},t.prototype.slug=function(e){var t=e.toLowerCase().trim().replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t))for(var n=t;this.seen[n]++,t=n+"-"+this.seen[n],this.seen.hasOwnProperty(t););return this.seen[t]=0,t},_.escapeTest=/[&<>"']/,_.escapeReplace=/[&<>"']/g,_.replacements={"&":"&","<":"<",">":">",'"':""","'":"'"},_.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,_.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var o={},u=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function g(){}function f(e){for(var t,n,r=1;r<arguments.length;r++)for(n in t=arguments[r])Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e}function y(e,t){var n=e.replace(/\|/g,function(e,t,n){for(var r=!1,s=t;0<=--s&&"\\"===n[s];)r=!r;return r?"|":" |"}).split(/ \|/),r=0;if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(/\\\|/g,"|");return n}function w(e,t,n){if(0===e.length)return"";for(var r=0;r<e.length;){var s=e.charAt(e.length-r-1);if(s!==t||n){if(s===t||!n)break;r++}else r++}return e.substr(0,e.length-r)}function d(e,t){if(-1===e.indexOf(t[1]))return-1;for(var n=0,r=0;r<e.length;r++)if("\\"===e[r])r++;else if(e[r]===t[0])n++;else if(e[r]===t[1]&&--n<0)return r;return-1}function m(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")}function k(e,n,r){if(null==e)throw new Error("marked(): input parameter is undefined or null");if("string"!=typeof e)throw new Error("marked(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected");if(r||"function"==typeof n){r||(r=n,n=null),m(n=f({},k.defaults,n||{}));var s,i,l=n.highlight,t=0;try{s=a.lex(e,n)}catch(e){return r(e)}i=s.length;var o=function(t){if(t)return n.highlight=l,r(t);var e;try{e=h.parse(s,n)}catch(e){t=e}return n.highlight=l,t?r(t):r(null,e)};if(!l||l.length<3)return o();if(delete n.highlight,!i)return o();for(;t<s.length;t++)!function(n){"code"!==n.type?--i||o():l(n.text,n.lang,function(e,t){return e?o(e):null==t||t===n.text?--i||o():(n.text=t,n.escaped=!0,void(--i||o()))})}(s[t])}else try{return n&&(n=f({},k.defaults,n)),m(n),h.parse(a.lex(e,n),n)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",(n||k.defaults).silent)return"<p>An error occurred:</p><pre>"+_(e.message+"",!0)+"</pre>";throw e}}g.exec=g,k.options=k.setOptions=function(e){return f(k.defaults,e),k},k.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,xhtml:!1}},k.defaults=k.getDefaults(),k.Parser=h,k.parser=h.parse,k.Renderer=r,k.TextRenderer=s,k.Lexer=a,k.lexer=a.lex,k.InlineLexer=p,k.inlineLexer=p.output,k.Slugger=t,k.parse=k,"undefined"!=typeof module&&"object"==typeof exports?module.exports=k:"function"==typeof define&&define.amd?define(function(){return k}):e.marked=k}(this||("undefined"!=typeof window?window:global)); + </script> + <script src="qrc:/qtwebchannel/qwebchannel.js"></script> + <style> + body{ + margin: 0 auto; + background: #2D2D30; + font-family: Sans-serif; + color: #F1F1F1; + line-height: 1; + padding-left: 10px; + padding-top: 0px; + } + h1, h2, h3, h4 { + font-weight: 400; + } + h1, h2, h3, h4, h5, p { + margin-bottom: 24px; + padding: 0; + } + h1 { + font-size: 48px; + } + h2 { + font-size: 36px; + margin: 24px 0 6px; + } + h3 { + font-size: 24px; + } + h4 { + font-size: 21px; + } + h5 { + font-size: 18px; + } + a { + color: #61BFC1; + margin: 0; + padding: 0; + text-decoration: none; + vertical-align: baseline; + } + a:hover { + text-decoration: underline; + } + a:visited { + color: #466B6C; + } + ul, ol { + padding: 0; + padding-left: 20px; + margin: 0; + } + li { + line-height: 24px; + } + li ul, li ul { + margin-left: 24px; + } + p, ul, ol { + font-size: 14px; + line-height: 24px; + } + pre { + padding: 0px 24px; + white-space: pre-wrap; + } + code { + font-family: Consolas, Monaco, Andale Mono, monospace; + line-height: 1.5; + font-size: 13px; + } + aside { + display: block; + float: right; + width: 390px; + } + blockquote { + border-left:.5em solid #eee; + padding: 0 2em; + margin-left:0; + } + blockquote cite { + font-size:14px; + line-height:20px; + color:#bfbfbf; + } + blockquote cite:before { + content: '\2014 \00A0'; + } + + blockquote p { + color: #666; + } + hr { + width: 540px; + text-align: left; + margin: 0 auto 0 0; + color: #999; + } + + /* Code below this line is copyright Twitter Inc. */ + + button, + input, + select, + textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; + } + button, input { + line-height: normal; + *overflow: visible; + } + button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; + } + button, + input[type="button"], + input[type="reset"], + input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; + } + input[type=checkbox], input[type=radio] { + cursor: pointer; + } + /* override default chrome & firefox settings */ + input:not([type="image"]), textarea { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + } + + input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + } + input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + label, + input, + select, + textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: normal; + margin-bottom: 18px; + } + input[type=checkbox], input[type=radio] { + cursor: pointer; + margin-bottom: 0; + } + input[type=text], + input[type=password], + textarea, + select { + display: inline-block; + width: 210px; + padding: 4px; + font-size: 13px; + font-weight: normal; + line-height: 18px; + height: 18px; + color: #808080; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + select, input[type=file] { + height: 27px; + line-height: 27px; + } + textarea { + height: auto; + } + + /* grey out placeholders */ + :-moz-placeholder { + color: #bfbfbf; + } + ::-webkit-input-placeholder { + color: #bfbfbf; + } + + input[type=text], + input[type=password], + select, + textarea { + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + } + input[type=text]:focus, input[type=password]:focus, textarea:focus { + outline: none; + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + } + + /* buttons */ + button { + display: inline-block; + padding: 4px 14px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #0064cd; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); + background-image: -moz-linear-gradient(top, #049cdb, #0064cd); + background-image: -ms-linear-gradient(top, #049cdb, #0064cd); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd)); + background-image: -webkit-linear-gradient(top, #049cdb, #0064cd); + background-image: -o-linear-gradient(top, #049cdb, #0064cd); + background-image: linear-gradient(top, #049cdb, #0064cd); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border: 1px solid #004b9a; + border-bottom-color: #003f81; + -webkit-transition: 0.1s linear all; + -moz-transition: 0.1s linear all; + transition: 0.1s linear all; + border-color: #0064cd #0064cd #003f81; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + } + button:hover { + color: #fff; + background-position: 0 -15px; + text-decoration: none; + } + button:active { + -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + } + button::-moz-focus-inner { + padding: 0; + border: 0; + } + </style> +</head> +<body> + <div id="placeholder"></div> + <script> + 'use strict'; + + var placeholder = document.getElementById('placeholder'); + + var updateText = function(text) { + placeholder.innerHTML = marked(text); + } + + new QWebChannel(qt.webChannelTransport, + function(channel) { + var content = channel.objects.content; + updateText(content.text); + content.textChanged.connect(updateText); + } + ); + </script> +</body> +</html>
\ No newline at end of file diff --git a/src/settings.cpp b/src/settings.cpp index 5aeb82fe..b533b400 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1977,6 +1977,17 @@ void DiagnosticsSettings::setLogLevel(log::Levels level) set(m_Settings, "Settings", "log_level", level); } +lootcli::LogLevels DiagnosticsSettings::lootLogLevel() const +{ + return get<lootcli::LogLevels>( + m_Settings, "Settings", "loot_log_level", lootcli::LogLevels::Info); +} + +void DiagnosticsSettings::setLootLogLevel(lootcli::LogLevels level) +{ + set(m_Settings, "Settings", "loot_log_level", level); +} + CrashDumpsType DiagnosticsSettings::crashDumpsType() const { return get<CrashDumpsType>(m_Settings, diff --git a/src/settings.h b/src/settings.h index d604823a..d71fabf4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #define SETTINGS_H #include "loadmechanism.h" +#include <lootcli/lootcli.h> #include <questionboxmemory.h> #include <log.h> #include <usvfsparameters.h> @@ -619,6 +620,10 @@ public: MOBase::log::Levels logLevel() const; void setLogLevel(MOBase::log::Levels level); + // log level for loot + lootcli::LogLevels lootLogLevel() const; + void setLootLogLevel(lootcli::LogLevels level); + // crash dump type for both MO and usvfs // CrashDumpsType crashDumpsType() const; diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 0ecbd101..b88c8b71 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -1386,44 +1386,23 @@ programs you are intentionally running.</string> <string>Diagnostics</string> </attribute> <layout class="QGridLayout" name="gridLayout_4"> - <item row="2" column="0"> - <layout class="QHBoxLayout" name="horizontalLayout_13"> - <item> - <widget class="QLabel" name="label_28"> - <property name="text"> - <string>Max Dumps To Keep</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_6"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>60</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QSpinBox" name="dumpsMaxEdit"> - <property name="toolTip"> - <string>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</string> - </property> - <property name="whatsThis"> - <string> - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - Set "Crash Dumps" above to None to disable crash dump collection. - </string> - </property> - </widget> - </item> - </layout> - </item> <item row="3" column="0"> + <spacer name="verticalSpacer_10"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>232</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0"> <widget class="QLabel" name="diagnosticsExplainedLabel"> <property name="toolTip"> <string>Hint: right click link and copy link location</string> @@ -1444,16 +1423,42 @@ programs you are intentionally running.</string> </property> </widget> </item> - <item row="1" column="0"> - <layout class="QHBoxLayout" name="horizontalLayout_12"> - <item> + <item row="0" column="0"> + <layout class="QFormLayout" name="formLayout_5"> + <property name="fieldGrowthPolicy"> + <enum>QFormLayout::ExpandingFieldsGrow</enum> + </property> + <property name="horizontalSpacing"> + <number>12</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>Log Level</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="logLevelBox"> + <property name="toolTip"> + <string>Decides the amount of data printed to "ModOrganizer.log"</string> + </property> + <property name="whatsThis"> + <string> + Decides the amount of data printed to "ModOrganizer.log". + "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. + </string> + </property> + </widget> + </item> + <item row="1" column="0"> <widget class="QLabel" name="label_27"> <property name="text"> <string>Crash Dumps</string> </property> </widget> </item> - <item> + <item row="1" column="1"> <widget class="QComboBox" name="dumpsTypeBox"> <property name="toolTip"> <string>Decides which type of crash dumps are collected when injected processes crash.</string> @@ -1469,46 +1474,36 @@ programs you are intentionally running.</string> </property> </widget> </item> - </layout> - </item> - <item row="4" column="0"> - <spacer name="verticalSpacer_10"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Expanding</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>232</height> - </size> - </property> - </spacer> - </item> - <item row="0" column="0"> - <layout class="QHBoxLayout" name="horizontalLayout_6"> - <item> - <widget class="QLabel" name="label_12"> + <item row="2" column="0"> + <widget class="QLabel" name="label_28"> <property name="text"> - <string>Log Level</string> + <string>Max Dumps To Keep</string> </property> </widget> </item> - <item> - <widget class="QComboBox" name="logLevelBox"> + <item row="2" column="1"> + <widget class="QSpinBox" name="dumpsMaxEdit"> <property name="toolTip"> - <string>Decides the amount of data printed to "ModOrganizer.log"</string> + <string>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</string> </property> <property name="whatsThis"> <string> - Decides the amount of data printed to "ModOrganizer.log". - "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. + Set "Crash Dumps" above to None to disable crash dump collection. </string> </property> </widget> </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_32"> + <property name="text"> + <string>LOOT Log Level</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QComboBox" name="lootLogLevel"/> + </item> </layout> </item> </layout> @@ -1589,9 +1584,6 @@ programs you are intentionally running.</string> <tabstop>bsaDateBtn</tabstop> <tabstop>execBlacklistBtn</tabstop> <tabstop>resetGeometryBtn</tabstop> - <tabstop>logLevelBox</tabstop> - <tabstop>dumpsTypeBox</tabstop> - <tabstop>dumpsMaxEdit</tabstop> </tabstops> <resources> <include location="resources.qrc"/> diff --git a/src/settingsdialogdiagnostics.cpp b/src/settingsdialogdiagnostics.cpp index 386c7425..74cadaa9 100644 --- a/src/settingsdialogdiagnostics.cpp +++ b/src/settingsdialogdiagnostics.cpp @@ -9,7 +9,8 @@ using namespace MOBase; DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d) { - setLevelsBox(); + setLogLevel(); + setLootLogLevel(); setCrashDumpTypesBox(); ui->dumpsMaxEdit->setValue(settings().diagnostics().crashDumpsMax()); @@ -26,7 +27,7 @@ DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d) ); } -void DiagnosticsSettingsTab::setLevelsBox() +void DiagnosticsSettingsTab::setLogLevel() { ui->logLevelBox->clear(); @@ -35,14 +36,40 @@ void DiagnosticsSettingsTab::setLevelsBox() ui->logLevelBox->addItem(QObject::tr("Warning"), log::Warning); ui->logLevelBox->addItem(QObject::tr("Error"), log::Error); + const auto sel = settings().diagnostics().logLevel(); + for (int i=0; i<ui->logLevelBox->count(); ++i) { - if (ui->logLevelBox->itemData(i) == settings().diagnostics().logLevel()) { + if (ui->logLevelBox->itemData(i) == sel) { ui->logLevelBox->setCurrentIndex(i); break; } } } +void DiagnosticsSettingsTab::setLootLogLevel() +{ + using L = lootcli::LogLevels; + + auto v = [](L level) { return QVariant(static_cast<int>(level)); }; + + ui->lootLogLevel->clear(); + + ui->lootLogLevel->addItem(QObject::tr("Trace"), v(L::Trace)); + ui->lootLogLevel->addItem(QObject::tr("Debug"), v(L::Debug)); + ui->lootLogLevel->addItem(QObject::tr("Info (recommended)"), v(L::Info)); + ui->lootLogLevel->addItem(QObject::tr("Warning"), v(L::Warning)); + ui->lootLogLevel->addItem(QObject::tr("Error"), v(L::Error)); + + const auto sel = settings().diagnostics().lootLogLevel(); + + for (int i=0; i<ui->lootLogLevel->count(); ++i) { + if (ui->lootLogLevel->itemData(i) == v(sel)) { + ui->lootLogLevel->setCurrentIndex(i); + break; + } + } +} + void DiagnosticsSettingsTab::setCrashDumpTypesBox() { ui->dumpsTypeBox->clear(); @@ -76,4 +103,7 @@ void DiagnosticsSettingsTab::update() static_cast<CrashDumpsType>(ui->dumpsTypeBox->currentData().toInt())); settings().diagnostics().setCrashDumpsMax(ui->dumpsMaxEdit->value()); + + settings().diagnostics().setLootLogLevel( + static_cast<lootcli::LogLevels>(ui->lootLogLevel->currentData().toInt())); } diff --git a/src/settingsdialogdiagnostics.h b/src/settingsdialogdiagnostics.h index f0fbf770..e01ee22f 100644 --- a/src/settingsdialogdiagnostics.h +++ b/src/settingsdialogdiagnostics.h @@ -12,7 +12,8 @@ public: void update(); private: - void setLevelsBox(); + void setLogLevel(); + void setLootLogLevel(); void setCrashDumpTypesBox(); }; |
