summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2019-11-25 04:01:32 -0500
committerGitHub <noreply@github.com>2019-11-25 04:01:32 -0500
commit765c9667de25547929f8ff58e3eab7bd3aeb3cbf (patch)
treeb8dc9f70a3a812fa3eb59461ab3bd65916b4b6b8
parent5f165b80e8035bafa15fe5a534726733836a3dd7 (diff)
parentf1b621d0babd33537cde97fc9d53e0dfa0ad5ea5 (diff)
Merge pull request #901 from isanae/loot-rework
Loot rework
-rw-r--r--src/CMakeLists.txt19
-rw-r--r--src/json.h190
-rw-r--r--src/loot.cpp847
-rw-r--r--src/loot.h133
-rw-r--r--src/lootdialog.cpp273
-rw-r--r--src/lootdialog.h78
-rw-r--r--src/lootdialog.ui241
-rw-r--r--src/mainwindow.cpp228
-rw-r--r--src/mainwindow.h4
-rw-r--r--src/pluginlist.cpp690
-rw-r--r--src/pluginlist.h76
-rw-r--r--src/resources/markdown.html292
-rw-r--r--src/settings.cpp11
-rw-r--r--src/settings.h5
-rw-r--r--src/settingsdialog.ui136
-rw-r--r--src/settingsdialogdiagnostics.cpp36
-rw-r--r--src/settingsdialogdiagnostics.h3
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={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},_.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 &quot;Crash Dumps&quot; 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 &quot;ModOrganizer.log&quot;</string>
+ </property>
+ <property name="whatsThis">
+ <string>
+ Decides the amount of data printed to &quot;ModOrganizer.log&quot;.
+ &quot;Debug&quot; 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 &quot;Info&quot; level for regular use. On the &quot;Error&quot; 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 &quot;ModOrganizer.log&quot;</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 &quot;ModOrganizer.log&quot;.
- &quot;Debug&quot; 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 &quot;Info&quot; level for regular use. On the &quot;Error&quot; level the log file usually remains empty.
+ Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
+ Set &quot;Crash Dumps&quot; 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();
};