aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_bsplugins/src/TESData
diff options
context:
space:
mode:
Diffstat (limited to 'libs/installer_bsplugins/src/TESData')
-rw-r--r--libs/installer_bsplugins/src/TESData/AssociatedEntry.cpp100
-rw-r--r--libs/installer_bsplugins/src/TESData/AssociatedEntry.h70
-rw-r--r--libs/installer_bsplugins/src/TESData/BranchConflictParser.cpp133
-rw-r--r--libs/installer_bsplugins/src/TESData/BranchConflictParser.h40
-rw-r--r--libs/installer_bsplugins/src/TESData/DataItem.cpp231
-rw-r--r--libs/installer_bsplugins/src/TESData/DataItem.h100
-rw-r--r--libs/installer_bsplugins/src/TESData/FileConflictParser.cpp228
-rw-r--r--libs/installer_bsplugins/src/TESData/FileConflictParser.h51
-rw-r--r--libs/installer_bsplugins/src/TESData/FileEntry.cpp227
-rw-r--r--libs/installer_bsplugins/src/TESData/FileEntry.h73
-rw-r--r--libs/installer_bsplugins/src/TESData/FileInfo.cpp177
-rw-r--r--libs/installer_bsplugins/src/TESData/FileInfo.h222
-rw-r--r--libs/installer_bsplugins/src/TESData/FormParser.cpp180
-rw-r--r--libs/installer_bsplugins/src/TESData/FormParser.h115
-rw-r--r--libs/installer_bsplugins/src/TESData/PluginList.cpp1429
-rw-r--r--libs/installer_bsplugins/src/TESData/PluginList.h200
-rw-r--r--libs/installer_bsplugins/src/TESData/Record.h90
-rw-r--r--libs/installer_bsplugins/src/TESData/RecordPath.cpp158
-rw-r--r--libs/installer_bsplugins/src/TESData/RecordPath.h86
-rw-r--r--libs/installer_bsplugins/src/TESData/SingleRecordParser.cpp179
-rw-r--r--libs/installer_bsplugins/src/TESData/SingleRecordParser.h52
-rw-r--r--libs/installer_bsplugins/src/TESData/TypeStringNames.h560
22 files changed, 4701 insertions, 0 deletions
diff --git a/libs/installer_bsplugins/src/TESData/AssociatedEntry.cpp b/libs/installer_bsplugins/src/TESData/AssociatedEntry.cpp
new file mode 100644
index 0000000..c11697c
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/AssociatedEntry.cpp
@@ -0,0 +1,100 @@
+#include "AssociatedEntry.h"
+
+#include <mutex>
+#include <shared_mutex>
+#include <vector>
+
+namespace TESData
+{
+
+AuxItem::AuxItem(const std::string& name, const AuxItem* parent)
+ : m_Name{name}, m_Parent{parent}
+{}
+
+std::shared_ptr<AuxItem> AuxItem::getByIndex(int index) const
+{
+ std::shared_lock lk{m_Mutex};
+
+ if (index < 0 || index >= m_Children.size()) {
+ return nullptr;
+ }
+
+ return m_Children.nth(index)->second;
+}
+
+std::shared_ptr<AuxItem> AuxItem::getByName(const std::string& name) const
+{
+ std::shared_lock lk{m_Mutex};
+
+ const auto it = m_Children.find(name);
+ if (it == m_Children.end()) {
+ return nullptr;
+ }
+
+ return it->second;
+}
+
+int AuxItem::indexOf(const AuxItem* item) const
+{
+ std::shared_lock lk{m_Mutex};
+
+ const auto it = std::ranges::find(m_Children, item, [&](auto&& pair) {
+ return pair.second.get();
+ });
+
+ return static_cast<int>(m_Children.index_of(it));
+}
+
+std::shared_ptr<AuxItem> AuxItem::insert(const std::string& name)
+{
+ std::unique_lock lk{m_Mutex};
+
+ const auto [it, inserted] =
+ m_Children.try_emplace(name, std::make_shared<AuxItem>(name, this));
+ return it->second;
+}
+
+std::shared_ptr<AuxMember> AuxItem::createMember(const std::string& path)
+{
+ std::unique_lock lk{m_Mutex};
+
+ auto& item = m_Member;
+ if (item != nullptr) {
+ return item;
+ }
+
+ item = std::make_shared<AuxMember>();
+ item->path = path;
+ return item;
+}
+
+void AuxItem::setMember(std::shared_ptr<AuxMember> item)
+{
+ m_Member = item;
+}
+
+AssociatedEntry::AssociatedEntry(const std::string& rootName)
+ : m_Root{std::make_shared<AuxItem>(rootName)}
+{}
+
+void AssociatedEntry::forEachMember(
+ std::function<void(const std::shared_ptr<const AuxMember>&)> func) const
+{
+ std::vector<std::shared_ptr<AuxItem>> stack{m_Root};
+
+ while (!stack.empty()) {
+ const auto item = std::move(stack.back());
+ stack.pop_back();
+
+ if (const auto member = item->member()) {
+ func(member);
+ }
+
+ for (int i = 0; i < item->numChildren(); ++i) {
+ const auto child = item->getByIndex(i);
+ stack.push_back(child);
+ }
+ }
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/AssociatedEntry.h b/libs/installer_bsplugins/src/TESData/AssociatedEntry.h
new file mode 100644
index 0000000..82c8323
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/AssociatedEntry.h
@@ -0,0 +1,70 @@
+#ifndef TESDATA_ASSOCIATEDENTRY_H
+#define TESDATA_ASSOCIATEDENTRY_H
+
+#include <boost/container/flat_map.hpp>
+
+#include <QString>
+
+#include <functional>
+#include <memory>
+#include <set>
+#include <shared_mutex>
+#include <string>
+
+namespace cont = boost::container;
+
+namespace TESData
+{
+
+using TESFileHandle = int;
+
+struct AuxMember
+{
+ std::string path;
+ std::set<TESFileHandle> alternatives;
+};
+
+class AuxItem final
+{
+public:
+ explicit AuxItem(const std::string& name, const AuxItem* parent = nullptr);
+
+ [[nodiscard]] const std::string& name() const { return m_Name; }
+ [[nodiscard]] const AuxItem* parent() const { return m_Parent; }
+
+ [[nodiscard]] int numChildren() const { return static_cast<int>(m_Children.size()); }
+ [[nodiscard]] std::shared_ptr<AuxItem> getByIndex(int index) const;
+ [[nodiscard]] std::shared_ptr<AuxItem> getByName(const std::string& name) const;
+ [[nodiscard]] int indexOf(const AuxItem* item) const;
+ std::shared_ptr<AuxItem> insert(const std::string& name);
+
+ [[nodiscard]] const auto& member() const { return m_Member; }
+ std::shared_ptr<AuxMember> createMember(const std::string& path);
+ void setMember(std::shared_ptr<AuxMember> item);
+
+private:
+ std::string m_Name;
+ const AuxItem* m_Parent;
+ cont::flat_map<std::string, std::shared_ptr<AuxItem>> m_Children;
+ std::shared_ptr<AuxMember> m_Member;
+ mutable std::shared_mutex m_Mutex;
+};
+
+class AssociatedEntry final
+{
+public:
+ explicit AssociatedEntry(const std::string& rootName = {});
+
+ [[nodiscard]] std::shared_ptr<AuxItem> root() { return m_Root; }
+ [[nodiscard]] std::shared_ptr<const AuxItem> root() const { return m_Root; }
+
+ void forEachMember(
+ std::function<void(const std::shared_ptr<const AuxMember>&)> func) const;
+
+private:
+ std::shared_ptr<AuxItem> m_Root;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_ASSOCIATEDENTRY_H
diff --git a/libs/installer_bsplugins/src/TESData/BranchConflictParser.cpp b/libs/installer_bsplugins/src/TESData/BranchConflictParser.cpp
new file mode 100644
index 0000000..7ea33de
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/BranchConflictParser.cpp
@@ -0,0 +1,133 @@
+#include "BranchConflictParser.h"
+
+#include <algorithm>
+#include <iterator>
+
+namespace TESData
+{
+
+BranchConflictParser::BranchConflictParser(PluginList* pluginList,
+ const std::string& pluginName,
+ const RecordPath& path)
+ : m_PluginList{pluginList}, m_PluginName{pluginName}, m_Path{path}
+{}
+
+bool BranchConflictParser::Group(TESFile::GroupData group)
+{
+ if (m_CurrentPath.groups().size() < m_Path.groups().size()) {
+ const auto& lastGroup = m_Path.groups()[m_CurrentPath.groups().size()];
+ if (group.type() != lastGroup.type()) {
+ return false;
+ }
+
+ if (group.hasParent()) {
+ const std::uint8_t localIndex = group.parent() >> 24U;
+ const std::string& owner =
+ localIndex < m_Masters.size() ? m_Masters[localIndex] : m_PluginName;
+
+ if (!TESFile::iequals(owner, m_Path.files()[lastGroup.parent() >> 24])) {
+ return false;
+ }
+ } else if (group != lastGroup) {
+ return false;
+ }
+ } else {
+ if (group.hasParent() && m_Path.hasFormId()) {
+ if ((group.parent() & 0xFFFFFF) != (m_Path.formId() & 0xFFFFFF)) {
+ return false;
+ }
+
+ const std::uint8_t localIndex = group.parent() >> 24U;
+ const std::string& owner =
+ localIndex < m_Masters.size() ? m_Masters[localIndex] : m_PluginName;
+
+ if (!TESFile::iequals(owner.data(), m_Path.files()[m_Path.formId() >> 24])) {
+ return false;
+ }
+ }
+ }
+
+ m_CurrentPath.push(group, m_Masters, m_PluginName);
+ return true;
+}
+
+void BranchConflictParser::EndGroup()
+{
+ m_CurrentPath.pop();
+}
+
+bool BranchConflictParser::Form(TESFile::FormData form)
+{
+ m_CurrentType = form.type();
+
+ if (m_CurrentPath.groups().empty()) {
+ return form.type() == "TES4"_ts;
+ }
+
+ if (m_CurrentPath.groups().size() < m_Path.groups().size()) {
+ return false;
+ } else if (m_CurrentPath.groups().size() == m_Path.groups().size()) {
+ if (m_Path.hasFormId()) {
+ if ((form.formId() & 0xFFFFFF) != (m_Path.formId() & 0xFFFFFF)) {
+ return false;
+ }
+
+ const std::uint8_t localIndex = form.formId() >> 24U;
+ const std::string& owner =
+ localIndex < m_Masters.size() ? m_Masters[localIndex] : m_PluginName;
+
+ if (!TESFile::iequals(owner, m_Path.files()[m_Path.formId() >> 24])) {
+ return false;
+ }
+ }
+ }
+
+ m_CurrentPath.setFormId(form.formId(), m_Masters, m_PluginName);
+
+ const std::uint8_t localModIndex = form.localModIndex();
+ const bool isMasterRecord = localModIndex < m_Masters.size();
+ return isMasterRecord;
+}
+
+void BranchConflictParser::EndForm()
+{
+ if (m_CurrentType != "TES4"_ts && m_CurrentType != "TES3"_ts) {
+
+ m_PluginList->addRecordConflict(m_PluginName, m_CurrentPath, m_CurrentType,
+ m_CurrentName);
+ }
+
+ m_CurrentPath.unsetFormId();
+ m_CurrentType = {};
+ m_CurrentChunk = {};
+ m_CurrentName.clear();
+}
+
+bool BranchConflictParser::Chunk(TESFile::Type type)
+{
+ m_CurrentChunk = type;
+ if (m_CurrentPath.groups().empty()) {
+ return type == "MAST"_ts;
+ } else {
+ return type == "EDID"_ts;
+ }
+}
+
+void BranchConflictParser::Data(std::istream& stream)
+{
+ switch (m_CurrentChunk) {
+ case "MAST"_ts: {
+ const std::string master = TESFile::readZstring(stream);
+ if (!master.empty()) {
+ m_Masters.push_back(master);
+ }
+ } break;
+
+ case "EDID"_ts: {
+ const std::string editorId = TESFile::readZstring(stream);
+ m_CurrentName = std::move(editorId);
+ } break;
+ }
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/BranchConflictParser.h b/libs/installer_bsplugins/src/TESData/BranchConflictParser.h
new file mode 100644
index 0000000..c2083bd
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/BranchConflictParser.h
@@ -0,0 +1,40 @@
+#ifndef TESDATA_BRANCHCONFLICTPARSER_H
+#define TESDATA_BRANCHCONFLICTPARSER_H
+
+#include "PluginList.h"
+#include "RecordPath.h"
+
+#include <istream>
+#include <vector>
+
+namespace TESData
+{
+
+class BranchConflictParser final
+{
+public:
+ BranchConflictParser(PluginList* pluginList, const std::string& pluginName,
+ const RecordPath& path);
+
+ bool Group(TESFile::GroupData group);
+ void EndGroup();
+ bool Form(TESFile::FormData form);
+ void EndForm();
+ bool Chunk(TESFile::Type type);
+ void Data(std::istream& stream);
+
+private:
+ PluginList* m_PluginList;
+ std::string m_PluginName;
+ TESData::RecordPath m_Path;
+
+ std::vector<std::string> m_Masters;
+ RecordPath m_CurrentPath;
+ TESFile::Type m_CurrentType;
+ TESFile::Type m_CurrentChunk;
+ std::string m_CurrentName;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_BRANCHCONFLICTPARSER_H
diff --git a/libs/installer_bsplugins/src/TESData/DataItem.cpp b/libs/installer_bsplugins/src/TESData/DataItem.cpp
new file mode 100644
index 0000000..c083aae
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/DataItem.cpp
@@ -0,0 +1,231 @@
+#include "DataItem.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace Qt::Literals::StringLiterals;
+
+namespace TESData
+{
+
+QString DataItem::makeName(TESFile::Type signature, const QString& name)
+{
+ const QByteArray sig =
+ QByteArray(signature.data(), signature.size()).toPercentEncoding("@"_ba);
+
+ if (!name.isEmpty()) {
+ return QStringLiteral("%1 - %2").arg(QString::fromLatin1(sig), name);
+ } else {
+ return QString::fromLatin1(sig);
+ }
+}
+
+QVariant DataItem::data(int fileIndex) const
+{
+ if (fileIndex < m_Data.size()) {
+ return m_Data[fileIndex];
+ }
+
+ return QVariant();
+}
+
+QVariant DataItem::displayData(int fileIndex) const
+{
+ if (fileIndex < m_DisplayData.size()) {
+ const auto& displayData = m_DisplayData[fileIndex];
+ if (displayData.isValid()) {
+ return displayData;
+ }
+ }
+
+ return data(fileIndex);
+}
+
+bool DataItem::isLosingConflict(int fileIndex, int fileCount) const
+{
+ if (m_ConflictType == ConflictType::Ignore ||
+ m_ConflictType == ConflictType::Benign ||
+ m_ConflictType == ConflictType::BenignIfAdded) {
+ return false;
+ }
+
+ if (!m_Data.isEmpty()) {
+ if (fileIndex >= m_Data.length()) {
+ return false;
+ } else if (fileCount > m_Data.length() &&
+ m_ConflictType != ConflictType::NormalIgnoreEmpty) {
+ return true;
+ }
+
+ for (int i = fileIndex + 1; i < m_Data.length(); ++i) {
+ if (hasConflict(m_Data[i], m_Data[fileIndex])) {
+ return true;
+ }
+ }
+ }
+
+ for (const auto& child : m_Children) {
+ if (child->isLosingConflict(fileIndex, fileCount)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool DataItem::isOverriding(int fileIndex) const
+{
+ if (m_ConflictType == ConflictType::Ignore) {
+ return false;
+ }
+
+ if (!m_Data.isEmpty()) {
+ if (fileIndex >= m_Data.length() &&
+ m_ConflictType != ConflictType::NormalIgnoreEmpty) {
+ return m_ConflictType != ConflictType::Benign;
+ }
+
+ for (int i = 0; i < std::min(fileIndex, static_cast<int>(m_Data.length())); ++i) {
+ if (m_ConflictType == ConflictType::Benign && !m_Data[i].isValid()) {
+ continue;
+ }
+
+ if (hasConflict(m_Data[i], m_Data[fileIndex])) {
+ return true;
+ }
+ }
+ }
+
+ for (const auto& child : m_Children) {
+ if (child->isOverriding(fileIndex)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool DataItem::isConflicted(int fileCount) const
+{
+ if (m_ConflictType == ConflictType::Ignore) {
+ return false;
+ }
+
+ if (!m_Data.isEmpty()) {
+ if (fileCount > m_Data.length() &&
+ m_ConflictType != ConflictType::NormalIgnoreEmpty) {
+ return true;
+ }
+
+ for (int i = 1; i < m_Data.length(); ++i) {
+ if (hasConflict(m_Data[i], m_Data[0])) {
+ return true;
+ }
+ }
+ }
+
+ for (const auto& child : m_Children) {
+ if (child->isConflicted(fileCount)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+DataItem* DataItem::findChild(TESFile::Type signature) const
+{
+ const auto it = std::ranges::find_if(m_Children, [&](auto&& child) {
+ return child->signature() == signature;
+ });
+
+ return it != std::end(m_Children) ? it->get() : nullptr;
+}
+
+QVariant DataItem::childData(TESFile::Type signature, int fileIndex) const
+{
+ const auto it = std::ranges::find_if(m_Children, [&](auto&& child) {
+ return child->signature() == signature;
+ });
+
+ if (it == std::end(m_Children)) {
+ return QVariant();
+ }
+
+ return (*it)->data(fileIndex);
+}
+
+QVariant DataItem::childData(const QString& name, int fileIndex) const
+{
+ const auto it = std::ranges::find_if(m_Children, [&](auto&& child) {
+ return child->name() == name;
+ });
+
+ if (it == std::end(m_Children)) {
+ return QVariant();
+ }
+
+ return (*it)->data(fileIndex);
+}
+
+int DataItem::indexOf(const DataItem* child) const
+{
+ const auto it = std::ranges::find(m_Children, child, &std::shared_ptr<DataItem>::get);
+ if (it == std::end(m_Children)) {
+ return -1;
+ }
+ return std::distance(std::begin(m_Children), it);
+}
+
+DataItem* DataItem::getOrInsertChild(int index, const QString& name,
+ ConflictType conflictType)
+{
+ if (index < m_Children.size()) {
+ const auto& child = m_Children[index];
+ if (child->name() == name) {
+ return child.get();
+ }
+ }
+ return insertChild(index, name, conflictType);
+}
+
+DataItem* DataItem::getOrInsertChild(int index, TESFile::Type signature,
+ const QString& name, ConflictType conflictType)
+{
+ if (index < m_Children.size()) {
+ const auto& child = m_Children[index];
+ if (child->signature() == signature) {
+ return child.get();
+ }
+ }
+ return insertChild(index, signature, name, conflictType);
+}
+
+void DataItem::setData(int fileIndex, const QVariant& data, bool caseSensitive)
+{
+ if (m_Data.size() <= fileIndex) {
+ m_Data.resize(fileIndex + 1);
+ }
+ m_Data[fileIndex] = data;
+ m_CaseSensitive = caseSensitive;
+}
+
+void DataItem::setDisplayData(int fileIndex, const QVariant& data)
+{
+ if (m_DisplayData.size() <= fileIndex) {
+ m_DisplayData.resize(fileIndex + 1);
+ }
+ m_DisplayData[fileIndex] = data;
+}
+
+bool DataItem::hasConflict(const QVariant& var1, const QVariant& var2) const
+{
+ if (!m_CaseSensitive && var1.userType() == QMetaType::QString &&
+ var2.userType() == QMetaType::QString) {
+ return var1.toString().compare(var2.toString(), Qt::CaseInsensitive) != 0;
+ } else {
+ return var1 != var2;
+ }
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/DataItem.h b/libs/installer_bsplugins/src/TESData/DataItem.h
new file mode 100644
index 0000000..824a096
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/DataItem.h
@@ -0,0 +1,100 @@
+#ifndef TESDATA_DATAITEM_H
+#define TESDATA_DATAITEM_H
+
+#include "TESFile/Type.h"
+
+#include <QList>
+#include <QString>
+#include <QVariant>
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace TESData
+{
+
+class DataItem final
+{
+public:
+ enum class ConflictType
+ {
+ Ignore,
+ BenignIfAdded,
+ Benign,
+ Override,
+ Translate,
+ NormalIgnoreEmpty,
+ Critical,
+ FormID,
+ };
+
+ DataItem() : m_Parent{nullptr} {}
+
+ DataItem(DataItem* parent, const QString& name, ConflictType conflictType)
+ : m_ConflictType{conflictType}, m_Name{name}, m_Parent{parent}
+ {}
+
+ DataItem(DataItem* parent, TESFile::Type signature, const QString& name,
+ ConflictType conflictType)
+ : m_ConflictType{conflictType}, m_Signature{signature},
+ m_Name{makeName(signature, name)}, m_Parent{parent}
+ {}
+
+ [[nodiscard]] static QString makeName(TESFile::Type signature, const QString& name);
+
+ [[nodiscard]] ConflictType conflictType() const { return m_ConflictType; }
+ [[nodiscard]] TESFile::Type signature() const { return m_Signature; }
+ [[nodiscard]] QString name() const { return m_Name; }
+ [[nodiscard]] DataItem* parent() const { return m_Parent; }
+ [[nodiscard]] int numChildren() const { return static_cast<int>(m_Children.size()); }
+ [[nodiscard]] DataItem* childAt(int index) const { return m_Children[index].get(); }
+ [[nodiscard]] int index() const { return m_Parent ? m_Parent->indexOf(this) : 0; }
+
+ [[nodiscard]] QVariant rowHeader() const { return name(); }
+
+ [[nodiscard]] QVariant data(int fileIndex) const;
+ [[nodiscard]] QVariant displayData(int fileIndex) const;
+
+ [[nodiscard]] bool isLosingConflict(int fileIndex, int fileCount) const;
+ [[nodiscard]] bool isOverriding(int fileIndex) const;
+ [[nodiscard]] bool isConflicted(int fileCount) const;
+
+ [[nodiscard]] DataItem* findChild(TESFile::Type signature) const;
+ [[nodiscard]] QVariant childData(TESFile::Type signature, int fileIndex) const;
+ [[nodiscard]] QVariant childData(const QString& name, int fileIndex) const;
+ [[nodiscard]] int indexOf(const DataItem* child) const;
+
+ template <typename... Args>
+ DataItem* insertChild(int index, Args&&... args)
+ {
+ const auto it = m_Children.insert(
+ m_Children.begin() + index,
+ std::make_shared<DataItem>(this, std::forward<Args>(args)...));
+ return it->get();
+ }
+
+ DataItem* getOrInsertChild(int index, const QString& name,
+ ConflictType conflictType = ConflictType::Override);
+ DataItem* getOrInsertChild(int index, TESFile::Type signature, const QString& name,
+ ConflictType conflictType = ConflictType::Override);
+
+ void setData(int fileIndex, const QVariant& data, bool caseSensitive = false);
+ void setDisplayData(int fileIndex, const QVariant& data);
+
+private:
+ [[nodiscard]] bool hasConflict(const QVariant& var1, const QVariant& var2) const;
+
+ ConflictType m_ConflictType{ConflictType::Override};
+ TESFile::Type m_Signature{};
+ bool m_CaseSensitive = false;
+ QString m_Name;
+ QList<QVariant> m_Data;
+ QList<QVariant> m_DisplayData;
+ DataItem* m_Parent;
+ std::vector<std::shared_ptr<DataItem>> m_Children;
+};
+
+} // namespace TESData
+
+#endif // BSPLUGININFO_DATAITEM_H
diff --git a/libs/installer_bsplugins/src/TESData/FileConflictParser.cpp b/libs/installer_bsplugins/src/TESData/FileConflictParser.cpp
new file mode 100644
index 0000000..e4958e4
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FileConflictParser.cpp
@@ -0,0 +1,228 @@
+#include "FileConflictParser.h"
+#include "PluginList.h"
+
+#include <log.h>
+
+#include <stdexcept>
+#include <string_view>
+
+namespace TESData
+{
+
+FileConflictParser::FileConflictParser(PluginList* pluginList, FileInfo* plugin,
+ bool lightSupported, bool mediumSupported,
+ bool blueprintSupported)
+ : m_PluginList{pluginList}, m_Plugin{plugin}, m_LightSupported{lightSupported},
+ m_MediumSupported{mediumSupported}, m_BlueprintSupported{blueprintSupported}
+{
+ m_PluginName = m_Plugin->name().toStdString();
+}
+
+bool FileConflictParser::Group(TESFile::GroupData group)
+{
+ if (group.hasDirectParent()) {
+ m_CurrentPath.push(group, m_Masters, m_PluginName);
+ m_PluginList->addGroupPlaceholder(m_PluginName, m_CurrentPath);
+ m_CurrentPath.pop();
+ return false;
+ }
+
+ if (m_Masters.empty() && group.hasFormType() && group.formType() != "GMST"_ts &&
+ group.formType() != "DOBJ"_ts) {
+ return false;
+ }
+
+ if (group.hasFormType() && group.formType() == "NAVI"_ts) {
+ return false;
+ }
+
+ m_CurrentPath.push(group, m_Masters, m_PluginName);
+ return true;
+}
+
+void FileConflictParser::EndGroup()
+{
+ m_CurrentPath.pop();
+}
+
+bool FileConflictParser::Form(TESFile::FormData form)
+{
+ m_CurrentType = form.type();
+
+ if (m_CurrentPath.groups().empty()) {
+ if (form.type() == "TES4"_ts) {
+ m_Plugin->setMasterFlagged(form.flags() & TESFile::RecordFlags::Master);
+ m_Plugin->setMediumFlagged(m_MediumSupported &&
+ (form.flags() & TESFile::RecordFlags::Medium));
+ m_Plugin->setBlueprintFlagged(m_BlueprintSupported &&
+ (form.flags() & TESFile::RecordFlags::Blueprint));
+ m_Plugin->setLightFlagged(
+ m_MediumSupported ? (form.flags() & TESFile::RecordFlags::SmallNew)
+ : m_LightSupported ? (form.flags() & TESFile::RecordFlags::SmallOld)
+ : false);
+ m_Plugin->setFormVersion(form.formVersion());
+ return true;
+ } else {
+ throw std::runtime_error("Unsupported header record");
+ }
+ }
+
+ switch (m_CurrentPath.groups().front().formType()) {
+ case "DOBJ"_ts:
+ case "GMST"_ts:
+ return true;
+ default:
+ m_CurrentPath.setFormId(form.formId(), m_Masters, m_PluginName);
+
+ const int localModIndex = form.localModIndex();
+ const bool isMasterRecord = localModIndex < m_Masters.size();
+ return isMasterRecord;
+ }
+}
+
+void FileConflictParser::EndForm()
+{
+ if (m_CurrentType != "TES4"_ts && m_CurrentType != "TES3"_ts &&
+ m_CurrentType != "GMST"_ts && m_CurrentType != "DOBJ"_ts) {
+
+ m_PluginList->addRecordConflict(m_PluginName, m_CurrentPath, m_CurrentType,
+ m_CurrentName);
+ }
+
+ m_CurrentPath.unsetFormId();
+ m_CurrentType = {};
+ m_CurrentChunk = {};
+ m_CurrentName.clear();
+}
+
+bool FileConflictParser::Chunk(TESFile::Type type)
+{
+ m_CurrentChunk = type;
+ if (m_CurrentPath.groups().empty()) {
+ switch (type) {
+ case "HEDR"_ts:
+ case "MAST"_ts:
+ case "CNAM"_ts:
+ case "SNAM"_ts:
+ return true;
+ }
+ return false;
+ } else if (m_CurrentPath.groups().front().formType() == "DOBJ"_ts) {
+ switch (type) {
+ case "DNAM"_ts:
+ return true;
+ }
+ return false;
+ } else {
+ switch (type) {
+ case "EDID"_ts:
+ return true;
+ }
+ return false;
+ }
+}
+
+void FileConflictParser::Data(std::istream& stream)
+{
+ if (m_CurrentPath.groups().empty()) {
+ return MainRecordData(stream);
+ }
+
+ switch (m_CurrentPath.groups().front().formType()) {
+ case "DOBJ"_ts:
+ return DefaultObjectData(stream);
+ case "GMST"_ts:
+ return GameSettingData(stream);
+ default:
+ return StandardData(stream);
+ }
+}
+
+void FileConflictParser::MainRecordData(std::istream& stream)
+{
+ switch (m_CurrentChunk) {
+ case "HEDR"_ts: {
+ struct Header
+ {
+ float version;
+ int32_t numRecords;
+ uint32_t nextObjectId;
+ };
+
+ const auto header = TESFile::readType<Header>(stream);
+ if (stream.fail()) {
+ MOBase::log::error("failed to read HEDR data");
+ return;
+ }
+
+ m_Plugin->setHasNoRecords(header.numRecords == 0);
+ m_Plugin->setHeaderVersion(header.version);
+ } break;
+
+ case "MAST"_ts: {
+ const std::string master = TESFile::readZstring(stream);
+ if (!master.empty()) {
+ m_Plugin->addMaster(QString::fromStdString(master.c_str()));
+ m_Masters.push_back(master);
+ }
+ } break;
+
+ case "CNAM"_ts: {
+ const std::string author = TESFile::readZstring(stream);
+ if (!author.empty()) {
+ m_Plugin->setAuthor(QString::fromLatin1(author.data()));
+ }
+ } break;
+
+ case "SNAM"_ts: {
+ const std::string desc = TESFile::readZstring(stream);
+ if (!desc.empty()) {
+ m_Plugin->setDescription(QString::fromLatin1(desc.data()));
+ }
+ } break;
+ }
+}
+
+void FileConflictParser::DefaultObjectData(std::istream& stream)
+{
+ switch (m_CurrentChunk) {
+ case "DNAM"_ts:
+ while (stream.peek() != std::char_traits<char>::eof()) {
+ const TESFile::Type name = TESFile::readType<TESFile::Type>(stream);
+ [[maybe_unused]] const std::uint32_t formId =
+ TESFile::readType<std::uint32_t>(stream);
+ if (name == TESFile::Type()) {
+ continue;
+ }
+ if (name == "BBBB"_ts) {
+ break;
+ }
+ m_CurrentPath.setTypeId(name);
+ m_PluginList->addRecordConflict(m_PluginName, m_CurrentPath, "DOBJ"_ts, "");
+ }
+ break;
+ }
+}
+
+void FileConflictParser::GameSettingData(std::istream& stream)
+{
+ switch (m_CurrentChunk) {
+ case "EDID"_ts: {
+ const std::string editorId = TESFile::readZstring(stream);
+ m_CurrentPath.setEditorId(editorId);
+ m_PluginList->addRecordConflict(m_PluginName, m_CurrentPath, "GMST"_ts, "");
+ } break;
+ }
+}
+
+void FileConflictParser::StandardData(std::istream& stream)
+{
+ switch (m_CurrentChunk) {
+ case "EDID"_ts: {
+ std::string editorId = TESFile::readZstring(stream);
+ m_CurrentName = std::move(editorId);
+ } break;
+ }
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/FileConflictParser.h b/libs/installer_bsplugins/src/TESData/FileConflictParser.h
new file mode 100644
index 0000000..fcea497
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FileConflictParser.h
@@ -0,0 +1,51 @@
+#ifndef TESDATA_FILECONFLICTPARSER_H
+#define TESDATA_FILECONFLICTPARSER_H
+
+#include "RecordPath.h"
+#include "TESFile/Stream.h"
+
+#include <string>
+#include <vector>
+
+namespace TESData
+{
+
+class PluginList;
+class FileInfo;
+
+class FileConflictParser final
+{
+public:
+ FileConflictParser(PluginList* pluginList, FileInfo* plugin, bool lightSupported,
+ bool mediumSupported, bool blueprintSupported);
+
+ bool Group(TESFile::GroupData group);
+ void EndGroup();
+ bool Form(TESFile::FormData form);
+ void EndForm();
+ bool Chunk(TESFile::Type type);
+ void Data(std::istream& stream);
+
+private:
+ void MainRecordData(std::istream& stream);
+ void DefaultObjectData(std::istream& stream);
+ void GameSettingData(std::istream& stream);
+ void StandardData(std::istream& stream);
+
+ PluginList* m_PluginList;
+ FileInfo* m_Plugin;
+ bool m_LightSupported;
+ bool m_MediumSupported;
+ bool m_BlueprintSupported;
+
+ std::string m_PluginName;
+ std::vector<std::string> m_Masters;
+ RecordPath m_CurrentPath;
+ TESFile::Type m_CurrentType;
+ TESFile::Type m_CurrentChunk;
+ std::string m_CurrentName;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_FILECONFLICTPARSER_H
diff --git a/libs/installer_bsplugins/src/TESData/FileEntry.cpp b/libs/installer_bsplugins/src/TESData/FileEntry.cpp
new file mode 100644
index 0000000..4eb1e02
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FileEntry.cpp
@@ -0,0 +1,227 @@
+#include "FileEntry.h"
+
+#include <algorithm>
+#include <iterator>
+#include <mutex>
+#include <shared_mutex>
+#include <utility>
+
+namespace TESData
+{
+
+FileEntry::FileEntry(TESFileHandle handle, const std::string& name)
+ : m_Handle{handle}, m_Name{name}, m_Root{std::make_shared<TreeItem>()}
+{}
+
+void FileEntry::forEachRecord(
+ std::function<void(const std::shared_ptr<const Record>&)> func) const
+{
+ std::vector<std::shared_ptr<TreeItem>> stack;
+ stack.push_back(m_Root);
+ while (!stack.empty()) {
+ const auto item = std::move(stack.back());
+ stack.pop_back();
+
+ if (item->record) {
+ func(item->record);
+ }
+
+ for (const auto& [identifier, child] : item->children) {
+ stack.push_back(child);
+ }
+ }
+}
+
+std::shared_ptr<Record> FileEntry::createRecord(const RecordPath& path,
+ const std::string& name,
+ TESFile::Type formType)
+{
+ const auto item = createHierarchy(path);
+
+ std::unique_lock lk{m_Mutex};
+
+ if (!item->record) {
+ item->record = std::make_shared<Record>();
+ lk.unlock();
+
+ item->record->setIdentifier(path.identifier(), path.files());
+ item->record->addAlternative(m_Handle);
+ item->name = name;
+ item->formType = formType;
+ }
+
+ return item->record;
+}
+
+void FileEntry::addRecord(const RecordPath& path, const std::string& name,
+ TESFile::Type formType, std::shared_ptr<Record> record)
+{
+ record->addAlternative(m_Handle);
+ const auto item = createHierarchy(path);
+ item->record = record;
+ item->name = name;
+ item->formType = formType;
+}
+
+void FileEntry::addChildGroup(const RecordPath& path)
+{
+ const auto item = findItem(path);
+ if (!item || !item->record) {
+ // no record to add children to
+ return;
+ }
+
+ TESFile::GroupData group = path.groups().back();
+ if (group.hasParent()) {
+ const auto& file = path.files()[group.parent() >> 24];
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(m_Files), TESFile::find(m_Files, file)));
+
+ if (newIndex == m_Files.size()) {
+ m_Files.push_back(file);
+ }
+
+ group.setLocalIndex(newIndex);
+ }
+
+ item->group = group;
+}
+
+std::shared_ptr<Record> FileEntry::findRecord(const RecordPath& path) const
+{
+ const auto item = findItem(path);
+ return item ? item->record : nullptr;
+}
+
+std::shared_ptr<FileEntry::TreeItem> FileEntry::findItem(const RecordPath& path) const
+{
+ std::shared_lock lk{m_Mutex};
+
+ const auto groups = path.groups();
+ auto item = m_Root;
+ for (TESFile::GroupData group : groups) {
+ if (group.hasParent()) {
+ const auto& file = path.files()[group.parent() >> 24];
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(m_Files), TESFile::find(m_Files, file)));
+
+ if (newIndex == m_Files.size()) {
+ return nullptr;
+ }
+
+ group.setLocalIndex(newIndex);
+ }
+
+ if (group.hasDirectParent() &&
+ (!item->record || item->record->formId() != group.parent())) {
+ if (const auto it = item->children.find(group.parent());
+ it != item->children.end()) {
+ item = it->second;
+ } else {
+ return nullptr;
+ }
+ } else {
+ if (const auto it = item->children.find(group); it != item->children.end()) {
+ item = it->second;
+ } else {
+ return nullptr;
+ }
+ }
+ }
+
+ TreeItem::Key key;
+ if (path.hasFormId()) {
+ const auto& file = path.files()[path.formId() >> 24];
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(m_Files), TESFile::find(m_Files, file)));
+
+ if (newIndex == m_Files.size()) {
+ return nullptr;
+ }
+
+ const std::uint32_t formId = (path.formId() & 0xFFFFFFU) | (newIndex << 24U);
+ key = formId;
+ } else if (path.hasEditorId()) {
+ key = path.editorId();
+ } else if (path.hasTypeId()) {
+ key = path.typeId();
+ } else {
+ return item;
+ }
+
+ const auto it = item->children.find(key);
+ return it != item->children.end() ? it->second : nullptr;
+}
+
+std::shared_ptr<FileEntry::TreeItem> FileEntry::createHierarchy(const RecordPath& path)
+{
+ std::unique_lock lk{m_Mutex};
+
+ const auto groups = path.groups();
+ auto item = m_Root;
+ for (TESFile::GroupData group : groups) {
+ if (group.hasParent()) {
+ const auto& file = path.files()[group.parent() >> 24];
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(m_Files), TESFile::find(m_Files, file)));
+
+ if (newIndex == m_Files.size()) {
+ m_Files.push_back(file);
+ }
+
+ group.setLocalIndex(newIndex);
+ }
+
+ if (group.hasDirectParent() &&
+ (!item->record || item->record->formId() != group.parent())) {
+ auto& nextItem = item->children[group.parent()];
+ if (!nextItem) {
+ nextItem = std::make_shared<TreeItem>();
+ nextItem->parent = item.get();
+ nextItem->record = std::make_shared<Record>();
+ nextItem->record->setIdentifier(group.parent(), m_Files);
+ }
+ nextItem->group = group;
+
+ item = nextItem;
+ } else {
+ auto& nextItem = item->children[group];
+ if (!nextItem) {
+ nextItem = std::make_shared<TreeItem>();
+ nextItem->parent = item.get();
+ nextItem->group = group;
+ }
+ item = nextItem;
+ }
+ }
+
+ TreeItem::Key key;
+ if (path.hasFormId()) {
+ const auto& file = path.files()[path.formId() >> 24];
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(m_Files), TESFile::find(m_Files, file)));
+
+ if (newIndex == m_Files.size()) {
+ m_Files.push_back(file);
+ }
+
+ const std::uint32_t formId = (path.formId() & 0xFFFFFFU) | (newIndex << 24U);
+ key = formId;
+ } else if (path.hasEditorId()) {
+ key = path.editorId();
+ } else if (path.hasTypeId()) {
+ key = path.typeId();
+ } else {
+ return item;
+ }
+
+ auto& recordItem = item->children[key];
+ if (!recordItem) {
+ recordItem = std::make_shared<TreeItem>();
+ recordItem->parent = item.get();
+ }
+
+ return recordItem;
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/FileEntry.h b/libs/installer_bsplugins/src/TESData/FileEntry.h
new file mode 100644
index 0000000..0ee6922
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FileEntry.h
@@ -0,0 +1,73 @@
+#ifndef TESDATA_FILEENTRY_H
+#define TESDATA_FILEENTRY_H
+
+#include "Record.h"
+#include "RecordPath.h"
+#include "TESFile/Type.h"
+
+#include <boost/container/flat_map.hpp>
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <shared_mutex>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace cont = boost::container;
+
+namespace TESData
+{
+
+using TESFileHandle = int;
+
+class FileEntry final
+{
+public:
+ struct TreeItem
+ {
+ using Key =
+ std::variant<TESFile::GroupData, std::uint32_t, std::string, TESFile::Type>;
+
+ const TreeItem* parent{nullptr};
+ std::string name;
+ TESFile::Type formType{};
+ std::optional<TESFile::GroupData> group;
+ std::shared_ptr<Record> record;
+ cont::flat_map<Key, std::shared_ptr<TreeItem>> children;
+ };
+
+ FileEntry(TESFileHandle handle, const std::string& name);
+
+ [[nodiscard]] TESFileHandle handle() const { return m_Handle; }
+ [[nodiscard]] const std::string& name() const { return m_Name; }
+ [[nodiscard]] TreeItem* dataRoot() const { return m_Root.get(); }
+ [[nodiscard]] const std::vector<std::string>& files() const { return m_Files; }
+
+ void
+ forEachRecord(std::function<void(const std::shared_ptr<const Record>&)> func) const;
+
+ std::shared_ptr<Record> createRecord(const RecordPath& path, const std::string& name,
+ TESFile::Type formType);
+ void addRecord(const RecordPath& path, const std::string& name,
+ TESFile::Type formType, std::shared_ptr<Record> record);
+ void addChildGroup(const RecordPath&);
+
+ [[nodiscard]] std::shared_ptr<Record> findRecord(const RecordPath& path) const;
+ [[nodiscard]] std::shared_ptr<TreeItem> findItem(const RecordPath& path) const;
+
+private:
+ std::shared_ptr<TreeItem> createHierarchy(const RecordPath& path);
+
+ TESFileHandle m_Handle;
+ std::string m_Name;
+ std::shared_ptr<TreeItem> m_Root;
+ std::vector<std::string> m_Files;
+ mutable std::shared_mutex m_Mutex;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_FILEENTRY_H
diff --git a/libs/installer_bsplugins/src/TESData/FileInfo.cpp b/libs/installer_bsplugins/src/TESData/FileInfo.cpp
new file mode 100644
index 0000000..8429a11
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FileInfo.cpp
@@ -0,0 +1,177 @@
+#include "FileInfo.h"
+#include "FileEntry.h"
+#include "MOPlugin/Settings.h"
+#include "PluginList.h"
+
+#include <algorithm>
+
+using namespace Qt::Literals::StringLiterals;
+
+namespace TESData
+{
+
+FileInfo::FileInfo(PluginList* pluginList, const QString& name, bool forceLoaded,
+ bool forceEnabled, bool forceDisabled, bool lightSupported)
+ : m_PluginList{pluginList},
+ m_FileSystemData{
+ .name = name,
+ .hasMasterExtension = name.endsWith(u".esm"_s, Qt::CaseInsensitive),
+ .hasLightExtension =
+ lightSupported && name.endsWith(u".esl"_s, Qt::CaseInsensitive),
+ .forceLoaded = forceLoaded,
+ .forceEnabled = forceEnabled,
+ .forceDisabled = forceDisabled,
+ },
+ m_Conflicts{[this]() {
+ return doConflictCheck();
+ }}
+{}
+
+bool FileInfo::isMasterFile() const
+{
+ return m_Metadata.isMasterFlagged || m_FileSystemData.hasMasterExtension ||
+ m_FileSystemData.hasLightExtension;
+}
+
+bool FileInfo::isMediumFile() const
+{
+ return m_Metadata.isMediumFlagged;
+}
+
+bool FileInfo::isSmallFile() const
+{
+ return m_Metadata.isLightFlagged || m_FileSystemData.hasLightExtension;
+}
+
+bool FileInfo::isBlueprintFile() const
+{
+ return isMasterFile() && m_Metadata.isBlueprintFlagged;
+}
+
+bool FileInfo::isAlwaysEnabled() const
+{
+ return m_FileSystemData.forceLoaded || m_FileSystemData.forceEnabled;
+}
+
+bool FileInfo::canBeToggled() const
+{
+ return !m_FileSystemData.forceLoaded && !m_FileSystemData.forceEnabled &&
+ !m_FileSystemData.forceDisabled;
+}
+
+bool FileInfo::mustLoadAfter(const FileInfo& other) const
+{
+ if (this->isBlueprintFile() && !other.isBlueprintFile()) {
+ return true;
+ } else if (other.isBlueprintFile() && !this->isBlueprintFile()) {
+ return false;
+ }
+
+ const bool hasMaster = this->masters().contains(other.name(), Qt::CaseInsensitive);
+ const bool isMaster = other.masters().contains(this->name(), Qt::CaseInsensitive);
+
+ if (hasMaster && !isMaster) {
+ return true;
+ } else if (isMaster) {
+ return false;
+ }
+
+ if (other.forceLoaded() && !this->forceLoaded()) {
+ return true;
+ }
+
+ if (other.isMasterFile() && !this->isMasterFile()) {
+ return true;
+ }
+
+ return false;
+}
+
+static void checkConflict(QSet<int>& winning, QSet<int>& losing, const FileInfo& file,
+ const TESData::PluginList* pluginList,
+ TESFileHandle alternative, bool ignoreMasters)
+{
+ const auto entry = pluginList->findEntryByName(file.name().toStdString());
+ const auto otherEntry = pluginList->findEntryByHandle(alternative);
+ if (otherEntry == nullptr || otherEntry == entry) {
+ return;
+ }
+
+ const auto otherFile =
+ pluginList->getPluginByName(QString::fromStdString(otherEntry->name()));
+ if (otherFile == nullptr) {
+ return;
+ }
+
+ const QString otherName = QString::fromStdString(otherEntry->name());
+ const int otherIndex = pluginList->getIndex(otherName);
+
+ if (file.priority() > otherFile->priority()) {
+ if (!ignoreMasters ||
+ !file.masters().contains(otherFile->name(), Qt::CaseInsensitive)) {
+ winning.insert(otherIndex);
+ }
+ } else {
+ if (!ignoreMasters ||
+ !otherFile->masters().contains(file.name(), Qt::CaseInsensitive)) {
+ losing.insert(otherIndex);
+ }
+ }
+}
+
+FileInfo::Conflicts FileInfo::doConflictCheck() const
+{
+ Conflicts conflicts;
+
+ const auto entry = m_PluginList->findEntryByName(name().toStdString());
+ if (entry == nullptr) {
+ return conflicts;
+ }
+
+ const bool ignoreMasters =
+ Settings::instance()->get<bool>("ignore_master_conflicts", false);
+
+ entry->forEachRecord([&](auto&& record) {
+ if (record->ignored())
+ return;
+
+ for (const auto alternative : record->alternatives()) {
+ checkConflict(conflicts.m_OverridingList, conflicts.m_OverriddenList, *this,
+ m_PluginList, alternative, ignoreMasters);
+ }
+ });
+
+ for (const auto& archive : m_FileSystemData.archives) {
+ const auto archiveEntry = m_PluginList->findArchive(archive);
+ if (!archiveEntry) {
+ continue;
+ }
+
+ archiveEntry->forEachMember([&](auto&& item) {
+ for (const auto alternative : item->alternatives) {
+ checkConflict(conflicts.m_OverwritingArchiveList,
+ conflicts.m_OverwrittenArchiveList, *this, m_PluginList,
+ alternative, ignoreMasters);
+ }
+ });
+ }
+
+ uint conflictState = CONFLICT_NONE;
+ if (!conflicts.m_OverridingList.empty()) {
+ conflictState |= CONFLICT_OVERRIDE;
+ }
+ if (!conflicts.m_OverriddenList.empty()) {
+ conflictState |= CONFLICT_OVERRIDDEN;
+ }
+ if (!conflicts.m_OverwritingArchiveList.empty()) {
+ conflictState |= CONFLICT_ARCHIVE_OVERWRITE;
+ }
+ if (!conflicts.m_OverwrittenArchiveList.empty()) {
+ conflictState |= CONFLICT_ARCHIVE_OVERWRITTEN;
+ }
+ conflicts.m_CurrentConflictState = static_cast<EConflictFlag>(conflictState);
+
+ return conflicts;
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/FileInfo.h b/libs/installer_bsplugins/src/TESData/FileInfo.h
new file mode 100644
index 0000000..57c5d60
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FileInfo.h
@@ -0,0 +1,222 @@
+#ifndef TESDATA_FILEINFO_H
+#define TESDATA_FILEINFO_H
+
+#include <ifiletree.h>
+#include <memoizedlock.h>
+
+#include <boost/container/flat_set.hpp>
+
+#include <QDateTime>
+#include <QSet>
+#include <QString>
+
+namespace TESData
+{
+
+class PluginList;
+
+class FileInfo
+{
+public:
+ enum EConflictFlag : uint
+ {
+ CONFLICT_NONE = 0x0,
+ CONFLICT_OVERRIDE = 0x1,
+ CONFLICT_OVERRIDDEN = 0x2,
+ CONFLICT_ARCHIVE_OVERWRITE = 0x4,
+ CONFLICT_ARCHIVE_OVERWRITTEN = 0x8,
+
+ CONFLICT_MIXED = CONFLICT_OVERRIDE | CONFLICT_OVERRIDDEN,
+ CONFLICT_ARCHIVE_MIXED = CONFLICT_ARCHIVE_OVERWRITE | CONFLICT_ARCHIVE_OVERWRITTEN,
+ };
+
+ enum EFlag : uint
+ {
+ FLAG_NONE = 0x000,
+ FLAG_PROBLEMATIC = 0x001,
+ FLAG_INFORMATION = 0x002,
+ FLAG_INI = 0x004,
+ FLAG_BSA = 0x008,
+ FLAG_MASTER = 0x010,
+ FLAG_MEDIUM = 0x020,
+ FLAG_LIGHT = 0x040,
+ FLAG_BLUEPRINT = 0x080,
+ FLAG_CLEAN = 0x100,
+ };
+
+ struct FileSystemData
+ {
+ QString name;
+
+ bool hasMasterExtension;
+ bool hasLightExtension;
+
+ bool forceLoaded;
+ bool forceEnabled;
+ bool forceDisabled;
+
+ bool hasIni;
+ boost::container::flat_set<QString, MOBase::FileNameComparator> archives;
+ };
+
+ struct Metadata
+ {
+ QString author;
+ QString description;
+
+ bool isMasterFlagged;
+ bool isMediumFlagged;
+ bool isLightFlagged;
+ bool isBlueprintFlagged;
+ bool hasNoRecords;
+
+ int formVersion;
+ float headerVersion;
+
+ QStringList masters;
+ mutable boost::container::flat_set<QString, MOBase::FileNameComparator> masterUnset;
+ };
+
+ struct State
+ {
+ bool enabled;
+ int priority = -1;
+ QString index;
+ int loadOrder;
+ QString group;
+
+ bool operator<(const State& other) const { return (loadOrder < other.loadOrder); }
+ };
+
+ struct Conflicts
+ {
+ EConflictFlag m_CurrentConflictState = CONFLICT_NONE;
+ QSet<int> m_OverridingList;
+ QSet<int> m_OverriddenList;
+ QSet<int> m_OverwritingArchiveList;
+ QSet<int> m_OverwrittenArchiveList;
+ };
+
+ FileInfo(PluginList* pluginList, const QString& name, bool forceLoaded,
+ bool forceEnabled, bool forceDisabled, bool lightSupported);
+
+ [[nodiscard]] const QString& name() const { return m_FileSystemData.name; }
+
+ [[nodiscard]] bool hasMasterExtension() const
+ {
+ return m_FileSystemData.hasMasterExtension;
+ }
+
+ [[nodiscard]] bool hasLightExtension() const
+ {
+ return m_FileSystemData.hasLightExtension;
+ }
+
+ [[nodiscard]] bool forceLoaded() const { return m_FileSystemData.forceLoaded; }
+ [[nodiscard]] bool forceEnabled() const { return m_FileSystemData.forceEnabled; }
+ [[nodiscard]] bool forceDisabled() const { return m_FileSystemData.forceDisabled; }
+
+ [[nodiscard]] bool hasIni() const { return m_FileSystemData.hasIni; }
+ void setHasIni(bool hasIni) { m_FileSystemData.hasIni = hasIni; }
+ [[nodiscard]] const auto& archives() const { return m_FileSystemData.archives; }
+ void addArchive(const QString& archive) { m_FileSystemData.archives.insert(archive); }
+
+ [[nodiscard]] const QString& author() const { return m_Metadata.author; }
+ void setAuthor(const QString& author) { m_Metadata.author = author; }
+ [[nodiscard]] const QString& description() const { return m_Metadata.description; }
+ void setDescription(const QString& text) { m_Metadata.description = text; }
+ [[nodiscard]] bool isMasterFlagged() const { return m_Metadata.isMasterFlagged; }
+ void setMasterFlagged(bool value) { m_Metadata.isMasterFlagged = value; }
+ [[nodiscard]] bool isMediumFlagged() const { return m_Metadata.isMediumFlagged; }
+ void setMediumFlagged(bool value) { m_Metadata.isMediumFlagged = value; }
+ [[nodiscard]] bool isLightFlagged() const { return m_Metadata.isLightFlagged; }
+ void setLightFlagged(bool value) { m_Metadata.isLightFlagged = value; }
+ [[nodiscard]] bool isBlueprintFlagged() const { return m_Metadata.isBlueprintFlagged; }
+ void setBlueprintFlagged(bool value) { m_Metadata.isBlueprintFlagged = value; }
+ [[nodiscard]] bool hasNoRecords() const { return m_Metadata.hasNoRecords; }
+ void setHasNoRecords(bool value) { m_Metadata.hasNoRecords = value; }
+ [[nodiscard]] int formVersion() const { return m_Metadata.formVersion; }
+ void setFormVersion(int value) { m_Metadata.formVersion = value; }
+ [[nodiscard]] float headerVersion() const { return m_Metadata.headerVersion; }
+ void setHeaderVersion(float value) { m_Metadata.headerVersion = value; }
+
+ [[nodiscard]] const auto& masters() const { return m_Metadata.masters; }
+ void addMaster(const QString& master) { m_Metadata.masters.push_back(master); }
+
+ [[nodiscard]] bool hasMissingMasters() const
+ {
+ return !m_Metadata.masterUnset.empty();
+ }
+
+ [[nodiscard]] const auto& missingMasters() const { return m_Metadata.masterUnset; }
+
+ template <std::ranges::input_range R>
+ void setMissingMasters(R&& range) const
+ {
+ m_Metadata.masterUnset.clear();
+ m_Metadata.masterUnset.insert(std::begin(range), std::end(range));
+ }
+
+ [[nodiscard]] bool enabled() const { return m_State.enabled; }
+ void setEnabled(bool enabled) { m_State.enabled = enabled; }
+ [[nodiscard]] int priority() const { return m_State.priority; }
+ void setPriority(int priority)
+ {
+ m_State.priority = priority;
+ m_Conflicts.invalidate();
+ }
+ [[nodiscard]] const QString& index() const { return m_State.index; }
+ void setIndex(const QString& index) { m_State.index = index; }
+ [[nodiscard]] int loadOrder() const { return m_State.loadOrder; }
+ void setLoadOrder(int loadOrder) { m_State.loadOrder = loadOrder; }
+ [[nodiscard]] const QString& group() const { return m_State.group; }
+ void setGroup(const QString& group) { m_State.group = group; }
+
+ [[nodiscard]] EConflictFlag conflictState() const
+ {
+ return m_Conflicts.value().m_CurrentConflictState;
+ }
+
+ [[nodiscard]] const auto& getPluginOverriding() const
+ {
+ return m_Conflicts.value().m_OverridingList;
+ }
+
+ [[nodiscard]] const auto& getPluginOverridden() const
+ {
+ return m_Conflicts.value().m_OverriddenList;
+ }
+
+ [[nodiscard]] const auto& getPluginOverwritingArchive() const
+ {
+ return m_Conflicts.value().m_OverwritingArchiveList;
+ }
+
+ [[nodiscard]] const auto& getPluginOverwrittenArchive() const
+ {
+ return m_Conflicts.value().m_OverwrittenArchiveList;
+ }
+
+ [[nodiscard]] bool isMasterFile() const;
+ [[nodiscard]] bool isMediumFile() const;
+ [[nodiscard]] bool isSmallFile() const;
+ [[nodiscard]] bool isBlueprintFile() const;
+ [[nodiscard]] bool isAlwaysEnabled() const;
+ [[nodiscard]] bool canBeToggled() const;
+ [[nodiscard]] bool mustLoadAfter(const FileInfo& other) const;
+
+ void invalidateConflicts() const { m_Conflicts.invalidate(); }
+
+private:
+ [[nodiscard]] Conflicts doConflictCheck() const;
+
+ PluginList* m_PluginList;
+ FileSystemData m_FileSystemData;
+ Metadata m_Metadata;
+ State m_State;
+ mutable MOBase::MemoizedLocked<Conflicts> m_Conflicts;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_FILEINFO_H
diff --git a/libs/installer_bsplugins/src/TESData/FormParser.cpp b/libs/installer_bsplugins/src/TESData/FormParser.cpp
new file mode 100644
index 0000000..08fc06b
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FormParser.cpp
@@ -0,0 +1,180 @@
+#include "FormParser.h"
+
+#include <bit>
+#include <ranges>
+#include <utility>
+
+namespace TESData
+{
+
+auto FormParserManager::registrationMap() -> RegistrationMap&
+{
+ static RegistrationMap registrationMap;
+ return registrationMap;
+}
+
+std::shared_ptr<const IFormParser> FormParserManager::getParser(Game game,
+ TESFile::Type type)
+{
+ if (game == Game::SSE) {
+ if (const auto it = registrationMap().find(type); it != registrationMap().end()) {
+ return it->second;
+ }
+ }
+
+ return registrationMap()[TESFile::Type()];
+}
+
+QString readBytes(std::istream& stream, int size)
+{
+ QString data;
+ for (int i = 0; i < size; ++i) {
+ char ch;
+ stream.get(ch);
+ if (stream.eof()) {
+ break;
+ }
+ data += u"%1 "_s.arg(static_cast<std::uint8_t>(ch), 2, 16, QChar(u'0')).toUpper();
+ }
+ return data;
+}
+
+QString readLstring(bool localized, std::istream& stream)
+{
+ if (localized) {
+ const std::uint32_t index = TESFile::readType<std::uint32_t>(stream);
+ if (index == 0) {
+ return u""_s;
+ }
+ return u"<lstring:%1>"_s.arg(index);
+ } else {
+ std::string str;
+ std::getline(stream, str, '\0');
+ return QString::fromStdString(str);
+ }
+}
+
+QString readFormId(std::span<const std::string> masters,
+ const std::string& plugin, std::istream& stream)
+{
+ const std::uint32_t formId = TESFile::readType<std::uint32_t>(stream);
+ if (!formId) {
+ return u"NONE"_s;
+ }
+
+ const std::uint8_t localIndex = formId >> 24U;
+ const std::string& file = localIndex < masters.size() ? masters[localIndex] : plugin;
+ return u"%2 | %1"_s.arg(formId & 0xFFFFFFU, 6, 16, QChar(u'0'))
+ .toUpper()
+ .arg(QString::fromStdString(file));
+}
+
+static void parseUnknown(DataItem* parent, int& index, int fileIndex,
+ TESFile::Type signature, std::istream& stream)
+{
+ DataItem* item = nullptr;
+ for (int i = index; i < parent->numChildren(); ++i) {
+ const auto child = parent->childAt(i);
+ if (child->signature() == signature) {
+ item = child;
+ index = i + 1;
+ break;
+ }
+ }
+
+ if (item == nullptr) {
+ item = parent->insertChild(index++, signature, u"Unknown"_s,
+ DataItem::ConflictType::Override);
+ }
+
+ item->setData(fileIndex, readBytes(stream, 256));
+}
+
+static void
+pushItem(DataItem*& item, std::vector<int>& indexStack, const QString& name,
+ DataItem::ConflictType conflictType = DataItem::ConflictType::Override,
+ bool alignable = true)
+{
+ if (alignable) {
+ item = item->getOrInsertChild(indexStack.back()++, name, conflictType);
+ } else {
+ item = item->insertChild(indexStack.back()++, name, conflictType);
+ }
+ indexStack.push_back(0);
+}
+
+static void
+pushItem(DataItem*& item, std::vector<int>& indexStack, TESFile::Type signature,
+ const QString& name,
+ DataItem::ConflictType conflictType = DataItem::ConflictType::Override,
+ bool alignable = true)
+{
+ if (alignable) {
+ item = item->getOrInsertChild(indexStack.back()++, signature, name, conflictType);
+ } else {
+ item = item->insertChild(indexStack.back()++, signature, name, conflictType);
+ }
+ indexStack.push_back(0);
+}
+
+static void popItem(DataItem*& item, std::vector<int>& indexStack)
+{
+ item = item->parent();
+ indexStack.pop_back();
+}
+
+[[nodiscard]] static QString readZstring(std::istream& stream)
+{
+ return QString::fromStdString(TESFile::readZstring(stream));
+}
+
+template <std::integral T>
+[[nodiscard]] static QString readWstring(std::istream& stream)
+{
+ const T length = TESFile::readType<T>(stream);
+ std::string str;
+ str.resize(length);
+ stream.read(str.data(), length);
+ return QString::fromStdString(str);
+}
+
+template <>
+void FormParser<>::parseFlags(DataItem* root, int fileIndex, std::uint32_t flags) const
+{
+ DataItem* item = root->getOrInsertChild(0, u"Record Flags"_s);
+
+ item = item->getOrInsertChild(0, u"Compressed"_s);
+ if (flags & (TESFile::RecordFlags::Compressed)) {
+ item->setData(fileIndex, u"Compressed"_s);
+ }
+}
+
+template <>
+ParseTask FormParser<>::parseForm(DataItem* root, int fileIndex,
+ [[maybe_unused]] bool localized,
+ [[maybe_unused]] std::span<const std::string> masters,
+ [[maybe_unused]] const std::string& plugin,
+ const TESFile::Type& signature,
+ std::istream* const& stream) const
+{
+ int index = 1;
+ for (;;) {
+ if (signature == "EDID"_ts) {
+ std::string editorId;
+ std::getline(*stream, editorId, '\0');
+ root->getOrInsertChild(index++, "EDID"_ts, u"Editor ID"_s)
+ ->setData(fileIndex, QString::fromStdString(editorId));
+ } else {
+ parseUnknown(root, index, fileIndex, signature, *stream);
+ }
+
+ co_await std::suspend_always();
+ }
+}
+
+} // namespace TESData
+
+#pragma warning(push)
+#pragma warning(disable : 4456)
+#include "FormParser.SSE.inl"
+#pragma warning(pop)
diff --git a/libs/installer_bsplugins/src/TESData/FormParser.h b/libs/installer_bsplugins/src/TESData/FormParser.h
new file mode 100644
index 0000000..b0220a0
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/FormParser.h
@@ -0,0 +1,115 @@
+#ifndef TESDATA_FORMPARSER_H
+#define TESDATA_FORMPARSER_H
+
+#include "DataItem.h"
+#include "TESFile/Stream.h"
+#include "TESFile/Type.h"
+
+#include <coroutine>
+#include <istream>
+#include <map>
+#include <memory>
+#include <span>
+#include <string>
+
+using namespace Qt::Literals::StringLiterals;
+
+namespace TESData
+{
+
+namespace Parse
+{
+ struct Promise;
+
+ struct Task : std::coroutine_handle<Promise>
+ {
+ using promise_type = Promise;
+
+ Task(std::coroutine_handle<Promise>&& promise)
+ : std::coroutine_handle<Promise>::coroutine_handle(promise)
+ {}
+ };
+
+ struct Promise
+ {
+ Task get_return_object() { return {Task::from_promise(*this)}; }
+ std::suspend_always initial_suspend() noexcept { return {}; }
+ std::suspend_always final_suspend() noexcept { return {}; }
+ void return_void() {}
+ void unhandled_exception() {}
+ };
+} // namespace Parse
+
+using ParseTask = Parse::Task;
+
+class IFormParser
+{
+public:
+ virtual void parseFlags(DataItem* root, int fileIndex, std::uint32_t flags) const = 0;
+
+ virtual ParseTask parseForm(DataItem* root, int fileIndex, bool localized,
+ std::span<const std::string> masters,
+ const std::string& plugin, const TESFile::Type& signature,
+ std::istream* const& stream) const = 0;
+};
+
+class FormParserManager final
+{
+ template <typename T, TESFile::Type Type>
+ friend struct RegisterFormParser;
+
+public:
+ enum class Game
+ {
+ TES4,
+ FO3,
+ FNV,
+ TES5,
+ FO4,
+ SSE,
+
+ Unknown
+ };
+
+ FormParserManager() = delete;
+
+ [[nodiscard]] static std::shared_ptr<const IFormParser> getParser(Game game,
+ TESFile::Type type);
+
+private:
+ using RegistrationMap = std::map<TESFile::Type, std::shared_ptr<IFormParser>>;
+
+ static auto registrationMap() -> RegistrationMap&;
+};
+
+template <typename T, TESFile::Type Type>
+struct [[maybe_unused]] RegisterFormParser
+{
+ explicit RegisterFormParser()
+ {
+ FormParserManager::registrationMap().emplace(Type, std::make_shared<T>());
+ }
+};
+
+template <TESFile::Type Type = {}>
+class FormParser : public IFormParser
+{
+ inline static RegisterFormParser<FormParser<Type>, Type> reg{};
+
+public:
+ void parseFlags(DataItem* root, int fileIndex, std::uint32_t flags) const override;
+
+ ParseTask parseForm(DataItem* root, int fileIndex, bool localized,
+ std::span<const std::string> masters, const std::string& plugin,
+ const TESFile::Type& signature,
+ std::istream* const& stream) const override;
+};
+
+QString readBytes(std::istream& stream, int size);
+QString readLstring(bool localized, std::istream& stream);
+QString readFormId(std::span<const std::string> masters, const std::string& plugin,
+ std::istream& stream);
+
+} // namespace TESData
+
+#endif // TESDATA_FORMPARSER_H
diff --git a/libs/installer_bsplugins/src/TESData/PluginList.cpp b/libs/installer_bsplugins/src/TESData/PluginList.cpp
new file mode 100644
index 0000000..805261c
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/PluginList.cpp
@@ -0,0 +1,1429 @@
+#include "PluginList.h"
+#include "FileConflictParser.h"
+#include "TESFile/Reader.h"
+
+#include <bsatk/bsatk.h>
+#include <game_features/gameplugins.h>
+#include <game_features/igamefeatures.h>
+#include <log.h>
+#include <safewritefile.h>
+#include <utility.h>
+
+#include <boost/container/flat_map.hpp>
+#include <boost/container/flat_set.hpp>
+#include <boost/thread/future.hpp>
+
+#include <QDir>
+#include <QFile>
+#include <QStringTokenizer>
+#include <QTextStream>
+
+#include <algorithm>
+#include <future>
+#include <iterator>
+#include <limits>
+#include <ranges>
+#include <semaphore>
+#include <thread>
+#include <tuple>
+#include <utility>
+
+using namespace Qt::Literals::StringLiterals;
+
+namespace TESData
+{
+
+#pragma region Constructor / Destructor
+
+PluginList::PluginList(const MOBase::IOrganizer* moInfo) : m_Organizer{moInfo}
+{
+ refresh(true);
+}
+
+PluginList::~PluginList() noexcept
+{
+ m_Refreshed.disconnect_all_slots();
+ m_PluginMoved.disconnect_all_slots();
+ m_PluginStateChanged.disconnect_all_slots();
+}
+
+#pragma endregion Constructor / Destructor
+#pragma region Plugin Access
+
+int PluginList::pluginCount() const
+{
+ return static_cast<int>(m_Plugins.size());
+}
+
+int PluginList::getIndex(const QString& pluginName) const
+{
+ const auto it = m_PluginsByName.find(pluginName);
+ return it != m_PluginsByName.end() ? it->second : -1;
+}
+
+int PluginList::getIndexAtPriority(int priority) const
+{
+ return m_PluginsByPriority.at(priority);
+}
+
+QString PluginList::getOriginName(int index) const
+{
+ if (index < 0 || index >= m_Plugins.size()) {
+ return QString();
+ }
+
+ const auto& name = m_Plugins[index]->name();
+ const auto origins = m_Organizer->getFileOrigins(name);
+ return !origins.isEmpty() ? origins.first() : QString();
+}
+
+const FileInfo* PluginList::getPlugin(int index) const
+{
+ if (index < 0 || index >= m_Plugins.size()) {
+ return nullptr;
+ }
+
+ return m_Plugins[index].get();
+}
+
+const FileInfo* PluginList::getPluginByName(const QString& name) const
+{
+ const auto it = m_PluginsByName.find(name);
+ if (it == m_PluginsByName.end()) {
+ return nullptr;
+ } else {
+ return m_Plugins.at(it->second).get();
+ }
+}
+
+const FileInfo* PluginList::getPluginByPriority(int priority) const
+{
+ if (priority < 0 || priority >= m_PluginsByPriority.size()) {
+ return nullptr;
+ }
+
+ return m_Plugins.at(m_PluginsByPriority[priority]).get();
+}
+
+FileInfo* PluginList::findPlugin(const QString& name)
+{
+ const auto it = m_PluginsByName.find(name);
+ if (it == m_PluginsByName.end()) {
+ return nullptr;
+ } else {
+ return m_Plugins.at(it->second).get();
+ }
+}
+
+const FileInfo* PluginList::findPlugin(const QString& name) const
+{
+ const auto it = m_PluginsByName.find(name);
+ if (it == m_PluginsByName.end()) {
+ return nullptr;
+ } else {
+ return m_Plugins.at(it->second).get();
+ }
+}
+
+#pragma endregion Plugin Access
+#pragma region Record Access
+
+FileEntry* PluginList::findEntryByName(const std::string& pluginName) const
+{
+ std::shared_lock lk{m_FileEntryMutex};
+ const auto it = m_EntriesByName.find(pluginName);
+ return it != m_EntriesByName.end() ? it->second.get() : nullptr;
+}
+
+FileEntry* PluginList::findEntryByHandle(TESFileHandle handle) const
+{
+ std::shared_lock lk{m_FileEntryMutex};
+ const auto it = m_EntriesByHandle.find(handle);
+ return it != m_EntriesByHandle.end() ? it->second.get() : nullptr;
+}
+
+AssociatedEntry* PluginList::findArchive(const QString& name) const
+{
+ std::shared_lock lk{m_ArchiveEntryMutex};
+ const auto it = m_Archives.find(name);
+ return it != m_Archives.end() ? it->second.get() : nullptr;
+}
+
+FileEntry* PluginList::createEntry(const std::string& name)
+{
+ std::unique_lock lk{m_FileEntryMutex};
+
+ const auto it = m_EntriesByName.find(name);
+ if (it != m_EntriesByName.end()) {
+ return it->second.get();
+ }
+
+ const auto entry = std::make_shared<FileEntry>(m_NextHandle++, name);
+ m_EntriesByHandle.emplace_hint(m_EntriesByHandle.cend(), entry->handle(), entry);
+ m_EntriesByName[name] = entry;
+ return entry.get();
+}
+
+void PluginList::addRecordConflict(const std::string& pluginName,
+ const RecordPath& path, TESFile::Type type,
+ const std::string& name)
+{
+ const auto& master = path.hasFormId() ? path.files()[path.formId() >> 24] : "";
+ const auto owner = createEntry(master);
+ const auto record = owner->createRecord(path, name, type);
+ if (pluginName != master) {
+ const auto entry = createEntry(pluginName);
+ entry->addRecord(path, name, type, record);
+ }
+}
+
+void PluginList::addGroupPlaceholder(const std::string& pluginName,
+ const RecordPath& path)
+{
+ const auto group = path.groups().back();
+ const auto& master = group.hasParent() ? path.files()[group.parent() >> 24] : "";
+ if (const auto owner = findEntryByName(master)) {
+ owner->addChildGroup(path);
+ }
+ if (pluginName != master) {
+ if (const auto entry = findEntryByName(pluginName)) {
+ entry->addChildGroup(path);
+ }
+ }
+}
+
+#pragma endregion Record Access
+#pragma region List Management
+
+QString PluginList::groupsPath() const
+{
+ const auto profilePath = QDir(m_Organizer->profilePath());
+ if (profilePath.isEmpty()) {
+ return QString();
+ }
+
+ return QDir::cleanPath(profilePath.absoluteFilePath(u"plugingroups.txt"_s));
+}
+
+QString PluginList::lockedOrderPath() const
+{
+ const auto profilePath = QDir(m_Organizer->profilePath());
+ if (profilePath.isEmpty()) {
+ return QString();
+ }
+
+ return QDir::cleanPath(profilePath.absoluteFilePath(u"lockedorder.txt"_s));
+}
+
+void PluginList::refresh(bool invalidate)
+{
+ MOBase::TimeThis tt{"TESData::PluginList::refresh()"};
+
+ m_Refreshing = true;
+ scanDataFiles(invalidate);
+ readPluginLists();
+
+ if (const auto groupsFile = groupsPath(); !groupsFile.isEmpty()) {
+ readGroups(groupsFile);
+ }
+
+ computeCompileIndices();
+ refreshLoadOrder();
+ dispatchPluginStateChanges();
+ testMasters();
+
+ if (const auto lockedOrderFile = lockedOrderPath(); !lockedOrderFile.isEmpty()) {
+ writeEmptyTextFile(lockedOrderFile);
+ }
+
+ m_Refreshing = false;
+ m_Refreshed();
+}
+
+void PluginList::setEnabled(int id, bool enable)
+{
+ const auto plugin = m_Plugins.at(id);
+
+ const bool enabled = plugin->enabled();
+ const bool shouldEnable =
+ (enable && !plugin->forceDisabled()) || plugin->isAlwaysEnabled();
+
+ if (shouldEnable != enabled) {
+ plugin->setEnabled(shouldEnable);
+ computeCompileIndices();
+ refreshLoadOrder();
+ pluginStatesChanged({plugin->name()}, shouldEnable ? STATE_ACTIVE : STATE_INACTIVE);
+ testMasters();
+ }
+}
+
+void PluginList::setEnabled(const std::vector<int>& ids, bool enable)
+{
+ QStringList changed;
+ for (const int id : ids) {
+ const auto plugin = m_Plugins.at(id);
+
+ const bool enabled = plugin->enabled();
+ const bool shouldEnable =
+ (enable && !plugin->forceDisabled()) || plugin->isAlwaysEnabled();
+
+ if (shouldEnable != enabled) {
+ plugin->setEnabled(shouldEnable);
+ changed.append(plugin->name());
+ }
+ }
+
+ if (!changed.isEmpty()) {
+ computeCompileIndices();
+ refreshLoadOrder();
+ pluginStatesChanged(changed, enable ? STATE_ACTIVE : STATE_INACTIVE);
+ testMasters();
+ }
+}
+
+void PluginList::toggleState(const std::vector<int>& ids)
+{
+ QStringList active;
+ QStringList inactive;
+ for (const int id : ids) {
+ const auto plugin = m_Plugins.at(id);
+
+ const bool enabled = plugin->enabled();
+ const bool shouldEnable =
+ (!enabled && !plugin->forceDisabled()) || plugin->isAlwaysEnabled();
+
+ if (shouldEnable != enabled) {
+ plugin->setEnabled(shouldEnable);
+ if (shouldEnable) {
+ active.append(plugin->name());
+ } else {
+ inactive.append(plugin->name());
+ }
+ }
+ }
+
+ if (!active.isEmpty() || !inactive.isEmpty()) {
+ computeCompileIndices();
+ refreshLoadOrder();
+ if (!active.isEmpty()) {
+ pluginStatesChanged(active, STATE_ACTIVE);
+ }
+ if (!inactive.isEmpty()) {
+ pluginStatesChanged(inactive, STATE_INACTIVE);
+ }
+ testMasters();
+ }
+}
+
+bool PluginList::canMoveToPriority(const std::vector<int>& ids, int newPriority) const
+{
+ boost::container::flat_set<QString, MOBase::FileNameComparator> names;
+ names.reserve(ids.size());
+ for (const int id : ids) {
+ const auto plugin = m_Plugins[id];
+ names.insert(plugin->name());
+ }
+
+ for (const int id : ids) {
+ const auto pluginToMove = m_Plugins[id];
+ const int priority = pluginToMove->priority();
+
+ if (pluginToMove->forceLoaded()) {
+ const int min = std::min(priority, newPriority);
+ const int max = std::max(priority, newPriority);
+ for (int i = min; i < max; ++i) {
+ if (std::ranges::find(ids, m_PluginsByPriority.at(i)) == std::end(ids)) {
+ return false;
+ }
+ }
+ }
+
+ for (int i = newPriority; i < priority; ++i) {
+ const auto plugin = m_Plugins.at(m_PluginsByPriority.at(i));
+ if (!names.contains(plugin->name()) && pluginToMove->mustLoadAfter(*plugin)) {
+ return false;
+ }
+ }
+
+ for (int i = priority + 1; i < newPriority; ++i) {
+ const auto plugin = m_Plugins.at(m_PluginsByPriority.at(i));
+ if (!names.contains(plugin->name()) && plugin->mustLoadAfter(*pluginToMove)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+QString PluginList::destinationGroup(
+ int oldPriority, int newPriority, const QString& originalGroup, bool isESM,
+ const boost::container::flat_set<QString, MOBase::FileNameComparator>& exclusions)
+{
+ const auto findPrevious = [&, this](int priority) -> FileInfo* {
+ for (int i = priority - 1; i >= 0; --i) {
+ const auto& plugin = m_Plugins.at(m_PluginsByPriority[i]);
+ if (!exclusions.contains(plugin->name())) {
+ return plugin.get();
+ }
+ }
+ return nullptr;
+ };
+
+ const auto findNext = [&, this](int priority) -> FileInfo* {
+ for (int i = priority + 1; i < m_PluginsByPriority.size(); ++i) {
+ const auto& plugin = m_Plugins.at(m_PluginsByPriority[i]);
+ if (!exclusions.contains(plugin->name())) {
+ return plugin.get();
+ }
+ }
+ return nullptr;
+ };
+
+ bool removedFromGroup = false;
+ if (!originalGroup.isEmpty()) {
+ if (const auto previous = findPrevious(oldPriority)) {
+ if (previous->group() == originalGroup && previous->isMasterFile() == isESM) {
+ removedFromGroup = true;
+ }
+ }
+
+ if (const auto next = findNext(oldPriority)) {
+ if (next->group() == originalGroup && next->isMasterFile() == isESM) {
+ removedFromGroup = true;
+ }
+ }
+ }
+
+ QString displacedGroup;
+ if (const auto displaced = m_Plugins.at(m_PluginsByPriority[newPriority])) {
+ displacedGroup = displaced->group();
+ }
+
+ QString neighborGroup;
+ if (newPriority < oldPriority) {
+ if (const auto neighbor = findPrevious(newPriority)) {
+ neighborGroup = neighbor->group();
+ }
+ } else if (newPriority > oldPriority) {
+ if (const auto neighbor = findNext(newPriority)) {
+ neighborGroup = neighbor->group();
+ }
+ }
+
+ if (!displacedGroup.isEmpty() && neighborGroup == displacedGroup) {
+ return displacedGroup;
+ }
+
+ if (!removedFromGroup || displacedGroup == originalGroup) {
+ return originalGroup;
+ }
+
+ return QString();
+}
+
+void PluginList::moveToPriority(std::vector<int> ids, int destination, bool disjoint)
+{
+ if (ids.empty()) {
+ return;
+ }
+
+ destination = std::max(destination, 0);
+ destination = std::min(destination, pluginCount());
+
+ boost::container::flat_set<QString, MOBase::FileNameComparator> names;
+ names.reserve(ids.size());
+ boost::container::flat_map<QString, int> priorities;
+ priorities.reserve(ids.size());
+ for (const int id : ids) {
+ const auto plugin = m_Plugins.at(id);
+ names.insert(plugin->name());
+ priorities.emplace(plugin->name(), plugin->priority());
+ }
+
+ std::ranges::sort(ids, [this](int lhs, int rhs) {
+ return m_Plugins[lhs]->priority() > m_Plugins[rhs]->priority();
+ });
+
+ int nextDestination = destination;
+ for (const int id : ids) {
+ const auto& pluginToMove = m_Plugins[id];
+ const int priority = pluginToMove->priority();
+
+ if (nextDestination < priority) {
+ for (int i = priority - 1; i >= nextDestination; --i) {
+ const auto& plugin = m_Plugins.at(m_PluginsByPriority.at(i));
+ if (!names.contains(plugin->name()) && pluginToMove->mustLoadAfter(*plugin)) {
+ nextDestination = i + 1;
+ break;
+ }
+ }
+
+ const int newPriority = nextDestination;
+ pluginToMove->setGroup(destinationGroup(priority, newPriority,
+ pluginToMove->group(),
+ pluginToMove->isMasterFile(), names));
+ for (int i = priority; i > nextDestination; --i) {
+ m_PluginsByPriority[i] = m_PluginsByPriority[i - 1];
+ m_Plugins[m_PluginsByPriority[i]]->setPriority(i);
+ }
+
+ m_PluginsByPriority[newPriority] = id;
+ pluginToMove->setPriority(newPriority);
+ } else if (nextDestination > priority) {
+ for (int i = priority + 1; i < nextDestination; ++i) {
+ const auto& plugin = m_Plugins.at(m_PluginsByPriority.at(i));
+ if (!names.contains(plugin->name()) && plugin->mustLoadAfter(*pluginToMove)) {
+ nextDestination = i;
+ break;
+ }
+ }
+
+ const int newPriority = --nextDestination;
+ pluginToMove->setGroup(destinationGroup(priority, newPriority,
+ pluginToMove->group(),
+ pluginToMove->isMasterFile(), names));
+ for (int i = priority; i < newPriority; ++i) {
+ m_PluginsByPriority[i] = m_PluginsByPriority[i + 1];
+ m_Plugins[m_PluginsByPriority[i]]->setPriority(i);
+ }
+
+ m_PluginsByPriority[newPriority] = id;
+ pluginToMove->setPriority(newPriority);
+ }
+
+ if (disjoint) {
+ nextDestination = destination;
+ }
+ }
+
+ computeCompileIndices();
+ refreshLoadOrder();
+
+ boost::container::flat_map<int, std::tuple<QString, int>> movedUp;
+ boost::container::flat_map<int, std::tuple<QString, int>, std::greater<int>>
+ movedDown;
+
+ for (const int id : ids) {
+ const auto& plugin = m_Plugins[id];
+ const auto& name = plugin->name();
+ const int oldPriority = priorities[name];
+ const int newPriority = plugin->priority();
+ if (newPriority < oldPriority) {
+ movedUp[oldPriority] = std::make_tuple(name, newPriority);
+ } else if (newPriority > oldPriority) {
+ movedDown[oldPriority] = std::make_tuple(name, newPriority);
+ }
+ }
+
+ for (auto& [oldPriority, moveInfo] : movedDown) {
+ auto& [name, newPriority] = moveInfo;
+ m_PluginMoved(name, oldPriority, newPriority);
+ }
+
+ for (auto& [oldPriority, moveInfo] : movedUp) {
+ auto& [name, newPriority] = moveInfo;
+ m_PluginMoved(name, oldPriority, newPriority);
+ }
+}
+
+void PluginList::shiftPriority(const std::vector<int>& ids, int offset)
+{
+ if (offset < 0) {
+ const int minPriority =
+ std::ranges::min(std::ranges::transform_view(ids, [this](auto&& id) {
+ return m_Plugins.at(id)->priority();
+ }));
+ moveToPriority(ids, minPriority + offset);
+ } else if (offset > 0) {
+ const int maxPriority =
+ std::ranges::max(std::ranges::transform_view(ids, [this](auto&& id) {
+ return m_Plugins.at(id)->priority();
+ }));
+ moveToPriority(ids, maxPriority + 1 + offset);
+ }
+}
+
+void PluginList::setGroup(const std::vector<int>& ids, const QString& group)
+{
+ for (const int id : ids) {
+ m_Plugins.at(id)->setGroup(group);
+ }
+
+ if (const auto groupsFile = groupsPath(); !groupsFile.isEmpty()) {
+ writeGroups(groupsFile);
+ }
+}
+
+QStringList PluginList::loadOrder() const
+{
+ QStringList result;
+
+ for (const int id : m_PluginsByPriority) {
+ result.append(m_Plugins.at(id)->name());
+ }
+
+ return result;
+}
+
+void PluginList::notifyPendingState(const QString& mod,
+ MOBase::IModList::ModStates state)
+{
+ if (state & MOBase::IModList::STATE_ACTIVE) {
+ m_PendingActive.insert(mod);
+ } else {
+ m_PendingActive.erase(mod);
+ }
+}
+
+void PluginList::flushPendingStates()
+{
+ m_PendingActive.clear();
+}
+
+#pragma endregion List Management
+#pragma region IPluginList
+
+QStringList PluginList::pluginNames() const
+{
+ QStringList result;
+
+ for (const auto& info : m_Plugins) {
+ result.append(info->name());
+ }
+
+ return result;
+}
+
+MOBase::IPluginList::PluginStates PluginList::state(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ if (!plugin) {
+ return IPluginList::STATE_MISSING;
+ } else {
+ return plugin->enabled() ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
+ }
+}
+
+void PluginList::setState(const QString& name, PluginStates state)
+{
+ const auto it = m_PluginsByName.find(name);
+ if (it == m_PluginsByName.end()) {
+ return;
+ }
+
+ const auto plugin = m_Plugins.at(it->second).get();
+
+ if (state == STATE_MISSING) {
+ m_Plugins.erase(m_Plugins.begin() + it->second);
+ } else {
+ const bool enabled = plugin->enabled();
+ const bool shouldEnable = (state == STATE_ACTIVE && !plugin->forceDisabled()) ||
+ plugin->isAlwaysEnabled();
+ if (shouldEnable == enabled) {
+ return;
+ }
+ plugin->setEnabled(shouldEnable);
+ }
+
+ queuePluginStateChange(plugin->name(), state);
+}
+
+int PluginList::priority(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->priority() : -1;
+}
+
+bool PluginList::setPriority(const QString& name, int newPriority)
+{
+ if (newPriority < 0 || newPriority >= static_cast<int>(m_PluginsByPriority.size())) {
+ MOBase::log::warn("Requested priority for \"{}\" out of range: {}", name,
+ newPriority);
+ return false;
+ }
+
+ const int oldPriority = priority(name);
+ if (oldPriority == -1) {
+ return false;
+ }
+
+ const int rowIndex = m_PluginsByPriority.at(oldPriority);
+
+ int destination = newPriority;
+ if (newPriority > oldPriority) {
+ ++destination;
+ }
+ moveToPriority({rowIndex}, destination);
+
+ return true;
+}
+
+int PluginList::loadOrder(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->loadOrder() : -1;
+}
+
+void PluginList::setLoadOrder(const QStringList& pluginList)
+{
+ for (const auto& info : m_Plugins) {
+ info->setPriority(-1);
+ }
+
+ int maxPriority = 0;
+ for (const QString& pluginName : pluginList) {
+ const auto plugin = const_cast<TESData::FileInfo*>(findPlugin(pluginName));
+ if (plugin != nullptr) {
+ plugin->setPriority(maxPriority++);
+ }
+ }
+
+ for (const auto& info : m_Plugins) {
+ if (info->priority() == -1) {
+ info->setPriority(maxPriority++);
+ }
+ }
+
+ updateCache();
+}
+
+bool PluginList::isMaster(const QString& name) const
+{
+ return isMasterFlagged(name);
+}
+
+QStringList PluginList::masters(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ QStringList result;
+ if (plugin != nullptr) {
+ for (const QString& master : plugin->masters()) {
+ result.append(master);
+ }
+ }
+ return result;
+}
+
+QString PluginList::origin(const QString& name) const
+{
+ if (m_PluginsByName.find(name) == m_PluginsByName.end()) {
+ return QString();
+ }
+
+ const auto origins = m_Organizer->getFileOrigins(name);
+ return !origins.isEmpty() ? origins.first() : QString();
+}
+
+bool PluginList::onRefreshed(const std::function<void()>& callback)
+{
+ auto connection = m_Refreshed.connect(callback);
+ return connection.connected();
+}
+
+bool PluginList::onPluginMoved(
+ const std::function<void(const QString&, int, int)>& func)
+{
+ auto connection = m_PluginMoved.connect(func);
+ return connection.connected();
+}
+
+bool PluginList::onPluginStateChanged(
+ const std::function<void(const std::map<QString, PluginStates>&)>& func)
+{
+ auto connection = m_PluginStateChanged.connect(func);
+ return connection.connected();
+}
+
+bool PluginList::hasMasterExtension(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->hasMasterExtension() : false;
+}
+
+bool PluginList::hasLightExtension(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->hasLightExtension() : false;
+}
+
+bool PluginList::isMasterFlagged(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->isMasterFlagged() : false;
+}
+
+bool PluginList::isMediumFlagged(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->isMediumFlagged() : false;
+}
+
+bool PluginList::isLightFlagged(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->isLightFlagged() : false;
+}
+
+bool PluginList::isBlueprintFlagged(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->isBlueprintFlagged() : false;
+}
+
+bool PluginList::hasNoRecords(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->hasNoRecords() : false;
+}
+
+int PluginList::formVersion(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->formVersion() : 0;
+}
+
+float PluginList::headerVersion(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->headerVersion() : 0.0;
+}
+
+QString PluginList::author(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->author() : QString();
+}
+
+QString PluginList::description(const QString& name) const
+{
+ const auto plugin = findPlugin(name);
+ return plugin ? plugin->description() : QString();
+}
+
+#pragma endregion IPluginList
+#pragma region ILootCache
+
+void PluginList::clearAdditionalInformation()
+{
+ m_LootInfo.clear();
+}
+
+void PluginList::addLootReport(const QString& name, MOTools::Loot::Plugin plugin)
+{
+ const auto it = m_PluginsByName.find(name);
+ if (it != m_PluginsByName.end()) {
+ m_LootInfo[name] = std::move(plugin);
+ } else {
+ MOBase::log::warn("failed to associate loot report for \"{}\"", name);
+ }
+}
+
+const MOTools::Loot::Plugin* PluginList::getLootReport(const QString& name) const
+{
+ const auto it = m_LootInfo.find(name);
+ return it != m_LootInfo.end() ? &it->second : nullptr;
+}
+
+#pragma endregion ILootCache
+#pragma region Slots
+
+void PluginList::writePluginLists() const
+{
+ const auto gameFeatures = m_Organizer->gameFeatures();
+ const auto tesSupport =
+ gameFeatures ? gameFeatures->gameFeature<MOBase::GamePlugins>() : nullptr;
+ if (tesSupport) {
+ tesSupport->writePluginLists(this);
+ }
+
+ if (const auto groupsFile = groupsPath(); !groupsFile.isEmpty()) {
+ writeGroups(groupsFile);
+ }
+}
+
+#pragma endregion Slots
+#pragma region Helpers
+
+static bool isPluginFile(const QString& filename)
+{
+ return filename.endsWith(u".esp"_s, Qt::CaseInsensitive) ||
+ filename.endsWith(u".esm"_s, Qt::CaseInsensitive) ||
+ filename.endsWith(u".esl"_s, Qt::CaseInsensitive);
+}
+
+enum class Game
+{
+ Skyrim,
+ Fallout4,
+ Starfield,
+ Other
+};
+
+static bool isAssociatedArchive(TESData::FileInfo& info, const QString& candidate,
+ Game game)
+{
+ const QString baseName = QFileInfo(info.name()).completeBaseName();
+ if (!candidate.startsWith(baseName)) {
+ return false;
+ }
+
+ switch (game) {
+ case Game::Skyrim:
+ if (candidate.endsWith(u".bsa"_s)) {
+ if (candidate.compare(baseName + u".bsa"_s, Qt::CaseInsensitive) == 0 ||
+ candidate.compare(baseName + u" - Textures.bsa"_s, Qt::CaseInsensitive) ==
+ 0) {
+ return true;
+ }
+ }
+ return false;
+
+ case Game::Fallout4:
+ case Game::Starfield:
+ if (candidate.endsWith(u".ba2"_s)) {
+ if (candidate.compare(baseName + u" - Main.ba2"_s, Qt::CaseInsensitive) == 0 ||
+ candidate.compare(baseName + u" - Textures.ba2"_s, Qt::CaseInsensitive) ==
+ 0 ||
+ candidate.startsWith(baseName + u" - Voices_"_s, Qt::CaseInsensitive)) {
+ return true;
+ }
+ }
+ return false;
+
+ default:
+ return candidate.endsWith(u".bsa"_s);
+ }
+}
+
+void PluginList::checkBsa(TESData::FileInfo& info,
+ const std::shared_ptr<const MOBase::IFileTree>& fileTree)
+{
+ const auto managedGame = m_Organizer->managedGame();
+ const auto gameName = managedGame ? managedGame->gameName() : QString();
+ const Game game = gameName.startsWith(u"Skyrim"_s) ? Game::Skyrim
+ : gameName.startsWith(u"Enderal"_s) ? Game::Skyrim
+ : gameName.startsWith(u"Fallout 4"_s) ? Game::Fallout4
+ : gameName == u"Starfield"_s ? Game::Starfield
+ : Game::Other;
+
+ const QString baseName = QFileInfo(info.name()).completeBaseName();
+ for (const auto entry : *fileTree) {
+ if (!entry) {
+ continue;
+ }
+
+ const auto candidate = entry->name();
+ if (isAssociatedArchive(info, candidate, game)) {
+ info.addArchive(candidate);
+ associateArchive(info, candidate);
+ }
+ }
+}
+
+static void checkIni(TESData::FileInfo& info,
+ const std::shared_ptr<const MOBase::IFileTree>& fileTree)
+{
+ QString baseName = QFileInfo(info.name()).completeBaseName();
+ QString iniPath = baseName + u".ini"_s;
+ if (fileTree->find(iniPath, MOBase::FileTreeEntry::FILE) != nullptr) {
+ info.setHasIni(true);
+ }
+}
+
+static void assignConsecutivePriorities(std::vector<std::shared_ptr<FileInfo>>& plugins)
+{
+ boost::container::flat_multimap<int, int> priorityToId;
+ for (int i = 0; i < plugins.size(); ++i) {
+ int priority = plugins[i]->priority();
+ if (priority == -1) {
+ priority = std::numeric_limits<int>::max();
+ }
+ priorityToId.emplace(priority, i);
+ }
+
+ int newPriority = 0;
+ for (auto& [priority, id] : priorityToId) {
+ plugins[id]->setPriority(newPriority++);
+ }
+}
+
+void PluginList::scanDataFiles(bool invalidate)
+{
+ if (invalidate) {
+ m_Plugins.clear();
+ m_PluginsByName.clear();
+ m_PluginsByPriority.clear();
+
+ m_EntriesByName.clear();
+ m_EntriesByHandle.clear();
+ m_NextHandle = 0;
+
+ m_MasterArchiveEntry = std::make_shared<AssociatedEntry>();
+ m_Archives.clear();
+ }
+
+ const auto managedGame = m_Organizer->managedGame();
+ const auto gameFeatures = m_Organizer->gameFeatures();
+
+ const QStringList primaryPlugins =
+ managedGame ? managedGame->primaryPlugins() : QStringList();
+ const QStringList enabledPlugins =
+ managedGame ? managedGame->enabledPlugins() : QStringList();
+ const auto loadOrderMechanism = managedGame
+ ? managedGame->loadOrderMechanism()
+ : MOBase::IPluginGame::LoadOrderMechanism::None;
+
+ const auto tesSupport =
+ gameFeatures ? gameFeatures->gameFeature<MOBase::GamePlugins>() : nullptr;
+
+ const bool lightPluginsAreSupported =
+ tesSupport && tesSupport->lightPluginsAreSupported();
+ const bool mediumPluginsAreSupported =
+ tesSupport && tesSupport->mediumPluginsAreSupported();
+ const bool blueprintPluginsAreSupported =
+ tesSupport && tesSupport->blueprintPluginsAreSupported();
+
+ QStringList availablePlugins;
+
+ const auto tree = m_Organizer->virtualFileTree();
+ for (const std::shared_ptr<const MOBase::FileTreeEntry> entry : *tree) {
+ if (entry == nullptr) {
+ continue;
+ }
+
+ const QString filename = entry->name();
+
+ if (!isPluginFile(filename)) {
+ continue;
+ }
+
+ if (m_Organizer->resolvePath(filename).isEmpty()) {
+ continue;
+ }
+
+ availablePlugins.append(filename);
+ }
+
+ for (const auto& modName : m_PendingActive) {
+ const auto modInterface = m_Organizer->modList()->getMod(modName);
+ const auto fileTree = modInterface ? modInterface->fileTree() : nullptr;
+
+ if (!fileTree)
+ continue;
+
+ std::shared_ptr<const MOBase::IFileTree> searchDir;
+ if (!m_Organizer->managedGame()->modDataDirectory().isEmpty()) {
+ searchDir =
+ fileTree->findDirectory(m_Organizer->managedGame()->modDataDirectory());
+ } else {
+ searchDir = fileTree;
+ }
+ if (searchDir) {
+ for (auto&& entry : *searchDir) {
+ if (entry && isPluginFile(entry->name()) &&
+ !availablePlugins.contains(entry->name(), Qt::CaseInsensitive)) {
+ availablePlugins.append(entry->name());
+ }
+ }
+ }
+ }
+
+ // Fluorine port: use ALL cores + force true async (upstream used hw/2 +
+ // default launch policy which libstdc++ may run as deferred → sequential).
+ const uint concurrency = std::max(1U, std::thread::hardware_concurrency());
+ std::counting_semaphore smph{concurrency};
+
+ std::vector<std::shared_future<void>> futures;
+ for (const auto& filename : availablePlugins) {
+ if (!invalidate && m_PluginsByName.contains(filename)) {
+ continue;
+ }
+
+ const bool forceLoaded = primaryPlugins.contains(filename, Qt::CaseInsensitive);
+ const bool forceEnabled = enabledPlugins.contains(filename, Qt::CaseInsensitive);
+ const bool forceDisabled =
+ !forceLoaded && !forceEnabled &&
+ (loadOrderMechanism == MOBase::IPluginGame::LoadOrderMechanism::None);
+
+ const QString fullPath = m_Organizer->resolvePath(filename);
+
+ const auto& info = m_Plugins.emplace_back(
+ std::make_shared<FileInfo>(this, filename, forceLoaded, forceEnabled,
+ forceDisabled, lightPluginsAreSupported));
+
+ auto assocTask = std::async(std::launch::async, [=, &smph] {
+ smph.acquire();
+ checkBsa(*info, tree);
+ checkIni(*info, tree);
+ smph.release();
+ });
+
+ auto fileTask = std::async(
+ std::launch::async, [=, this, &smph, path = fullPath.toStdString()] {
+ smph.acquire();
+
+ try {
+ FileConflictParser handler{this, info.get(), lightPluginsAreSupported,
+ mediumPluginsAreSupported,
+ blueprintPluginsAreSupported};
+ TESFile::Reader<FileConflictParser> reader{};
+ // Fluorine port: use UTF-8 bytes (upstream used toStdWString which
+ // silently mangles paths through the C locale on Linux). Also fall
+ // back to case-insensitive directory lookup — IOrganizer::resolvePath
+ // returns paths with the user's original casing, while the file on
+ // disk may differ (Windows mods unpacked on case-sensitive Linux).
+ std::filesystem::path fspath{path};
+ if (!std::filesystem::exists(fspath)) {
+ const auto parent = fspath.parent_path();
+ const auto wantName = fspath.filename().string();
+ if (std::filesystem::exists(parent)) {
+ for (const auto& entry : std::filesystem::directory_iterator{parent}) {
+ const auto name = entry.path().filename().string();
+ if (name.size() == wantName.size() &&
+ std::equal(name.begin(), name.end(), wantName.begin(),
+ [](unsigned char a, unsigned char b) {
+ return std::tolower(a) == std::tolower(b);
+ })) {
+ fspath = entry.path();
+ break;
+ }
+ }
+ }
+ }
+ reader.parse(fspath, handler);
+ } catch (const std::exception& e) {
+ MOBase::log::error("Error parsing \"{}\": {}", path, e.what());
+ }
+
+ smph.release();
+ });
+
+ futures.push_back(assocTask.share());
+ futures.push_back(fileTask.share());
+ }
+
+ boost::wait_for_all(futures.begin(), futures.end());
+
+ if (!invalidate) {
+ std::erase_if(m_Plugins, [&](auto&& plugin) {
+ return !plugin || !availablePlugins.contains(plugin->name(), Qt::CaseInsensitive);
+ });
+ }
+
+ assignConsecutivePriorities(m_Plugins);
+ updateCache();
+}
+
+void PluginList::readPluginLists()
+{
+ const auto gameFeatures = m_Organizer->gameFeatures();
+ const auto tesSupport =
+ gameFeatures ? gameFeatures->gameFeature<MOBase::GamePlugins>() : nullptr;
+
+ if (tesSupport) {
+ tesSupport->readPluginLists(this);
+ }
+
+ enforcePluginRelationships();
+}
+
+static void populateArchiveFiles(const std::shared_ptr<AuxItem>& master,
+ const std::shared_ptr<AuxItem>& entry,
+ const BSA::Folder::Ptr& archiveFolder,
+ TESFileHandle handle)
+{
+ for (unsigned int i = 0, num = archiveFolder->getNumFiles(); i < num; ++i) {
+ const auto file = archiveFolder->getFile(i);
+
+ const auto conflictItem =
+ master->insert(file->getName())->createMember(file->getFilePath());
+ conflictItem->alternatives.insert(handle);
+ entry->insert(file->getName())->setMember(conflictItem);
+ }
+
+ for (unsigned int i = 0, num = archiveFolder->getNumSubFolders(); i < num; ++i) {
+ const auto folder = archiveFolder->getSubFolder(i);
+ populateArchiveFiles(master->insert(folder->getName()),
+ entry->insert(folder->getName()), folder, handle);
+ }
+}
+
+void PluginList::associateArchive(const TESData::FileInfo& info,
+ const QString& archiveName)
+{
+ const auto archiveEntry = [this, &archiveName] {
+ std::unique_lock lk{m_ArchiveEntryMutex};
+ auto& entry = m_Archives[archiveName];
+ if (entry == nullptr) {
+ entry = std::make_shared<AssociatedEntry>();
+ }
+ return entry;
+ }();
+
+ const QString archivePath = m_Organizer->resolvePath(archiveName);
+ if (archivePath.isEmpty()) {
+ return;
+ }
+
+ BSA::Archive archive;
+ BSA::EErrorCode result = BSA::ERROR_NONE;
+
+ try {
+ result = archive.read(qPrintable(archivePath), false);
+ } catch (const std::exception& e) {
+ MOBase::log::error("invalid bsa '{}', error {}", archivePath, e.what());
+ return;
+ }
+
+ if (result != BSA::ERROR_NONE && result != BSA::ERROR_INVALIDHASHES) {
+ MOBase::log::error("invalid bsa '{}', error {}", archivePath, result);
+ return;
+ }
+
+ const auto handle = createEntry(info.name().toStdString())->handle();
+ populateArchiveFiles(m_MasterArchiveEntry->root(), archiveEntry->root(),
+ archive.getRoot(), handle);
+}
+
+void PluginList::clearGroups()
+{
+ for (const auto& plugin : m_Plugins) {
+ plugin->setGroup(QString());
+ }
+}
+
+void PluginList::readGroups(const QString& fileName)
+{
+ clearGroups();
+
+ QFile file{fileName};
+ if (!file.exists()) {
+ return;
+ }
+
+ int lineNumber = 0;
+ if (!file.open(QFile::ReadOnly)) {
+ return;
+ }
+
+ QTextStream stream{&file};
+
+ QString line;
+ while (stream.readLineInto(&line)) {
+ ++lineNumber;
+
+ if (line.length() <= 0 || line.at(0) == u'#') {
+ continue;
+ }
+
+ const auto fields = line.split(u'|');
+ if (fields.count() != 2) {
+ MOBase::log::error("plugin groups file: invalid line #{}: {}", lineNumber, line);
+ continue;
+ }
+
+ if (const auto it = m_PluginsByName.find(fields[0]); it != m_PluginsByName.end()) {
+ const auto& plugin = m_Plugins.at(it->second);
+ plugin->setGroup(fields[1]);
+ }
+ }
+
+ file.close();
+}
+
+void PluginList::writeEmptyTextFile(const QString& fileName) const
+{
+ MOBase::SafeWriteFile file{fileName};
+
+ file->resize(0);
+ file->write("# This file was automatically generated by Mod Organizer.\r\n"_ba);
+ file->commit();
+}
+
+void PluginList::writeGroups(const QString& fileName) const
+{
+ MOBase::SafeWriteFile file{fileName};
+
+ file->resize(0);
+ file->write("# This file was automatically generated by Mod Organizer.\r\n"_ba);
+ for (const auto& [name, i] : m_PluginsByName) {
+ const auto& plugin = m_Plugins.at(i);
+ const auto& group = plugin->group();
+ if (!group.isEmpty()) {
+ file->write(u"%1|%2\r\n"_s.arg(name).arg(group).toUtf8());
+ }
+ }
+
+ file->commit();
+}
+
+void PluginList::queuePluginStateChange(const QString& pluginName, PluginStates state)
+{
+ if (m_Refreshing) {
+ m_QueuedStateChanges[pluginName] = state;
+ } else {
+ computeCompileIndices();
+ refreshLoadOrder();
+ pluginStatesChanged({pluginName}, state);
+ testMasters();
+ }
+}
+
+void PluginList::dispatchPluginStateChanges()
+{
+ if (!m_QueuedStateChanges.empty()) {
+ m_PluginStateChanged(m_QueuedStateChanges);
+ m_QueuedStateChanges.clear();
+ }
+}
+
+void PluginList::pluginStatesChanged(const QStringList& pluginNames,
+ PluginStates state) const
+{
+ if (pluginNames.isEmpty()) {
+ return;
+ }
+
+ std::map<QString, IPluginList::PluginStates> infos;
+ for (const auto& name : pluginNames) {
+ infos[name] = state;
+ }
+
+ m_PluginStateChanged(infos);
+}
+
+void PluginList::enforcePluginRelationships()
+{
+ MOBase::TimeThis tt{"TESData::PluginList::enforcePluginRelationships"};
+
+ for (int i = 0; i < m_PluginsByPriority.size(); ++i) {
+ const int& firstIndex = m_PluginsByPriority[i];
+
+ for (int j = i + 1; j < m_PluginsByPriority.size(); ++j) {
+ const int secondIndex = m_PluginsByPriority[j];
+ const auto& firstPlugin = m_Plugins.at(firstIndex);
+ const auto& secondPlugin = m_Plugins.at(secondIndex);
+
+ if (firstPlugin->mustLoadAfter(*secondPlugin)) {
+ for (int k = j; k > i + 1; --k) {
+ m_PluginsByPriority[k] = m_PluginsByPriority[k - 1];
+ m_Plugins[m_PluginsByPriority[k]]->setPriority(k);
+ }
+ m_PluginsByPriority[i + 1] = firstIndex;
+ firstPlugin->setPriority(i + 1);
+ m_PluginsByPriority[i] = secondIndex;
+ secondPlugin->setPriority(i);
+ j = i;
+ }
+ }
+ }
+
+ computeCompileIndices();
+ refreshLoadOrder();
+}
+
+void PluginList::testMasters()
+{
+ boost::container::flat_set<QString, MOBase::FileNameComparator> enabledMasters;
+ for (const auto& plugin : m_Plugins) {
+ if (plugin->enabled()) {
+ enabledMasters.insert(plugin->name());
+ }
+ }
+
+ for (auto& plugin : m_Plugins) {
+ std::vector<QString> missingMasters;
+ std::ranges::remove_copy_if(plugin->masters(), std::back_inserter(missingMasters),
+ [&](auto&& file) {
+ return enabledMasters.contains(file);
+ });
+ plugin->setMissingMasters(missingMasters);
+ }
+}
+
+void PluginList::updateCache()
+{
+ m_PluginsByName.clear();
+ m_PluginsByPriority.clear();
+ m_PluginsByPriority.resize(m_Plugins.size());
+ for (int i = 0; i < m_Plugins.size(); ++i) {
+ if (m_Plugins[i]->priority() < 0) {
+ continue;
+ }
+ if (m_Plugins[i]->priority() >= static_cast<int>(m_Plugins.size())) {
+ MOBase::log::error("invalid plugin priority: {}", m_Plugins[i]->priority());
+ continue;
+ }
+ m_PluginsByName[m_Plugins[i]->name()] = i;
+ m_PluginsByPriority[m_Plugins[i]->priority()] = i;
+ }
+
+ computeCompileIndices();
+ refreshLoadOrder();
+}
+
+void PluginList::computeCompileIndices()
+{
+ int numNormal = 0;
+ int numESLs = 0;
+ int numESHs = 0;
+ int numSkipped = 0;
+
+ const auto gameFeatures = m_Organizer->gameFeatures();
+ const auto tesSupport =
+ gameFeatures ? gameFeatures->gameFeature<MOBase::GamePlugins>() : nullptr;
+
+ const bool lightPluginsAreSupported =
+ tesSupport && tesSupport->lightPluginsAreSupported();
+ const bool mediumPluginsAreSupported =
+ tesSupport && tesSupport->mediumPluginsAreSupported();
+
+ for (int priority = 0; priority < m_PluginsByPriority.size(); ++priority) {
+ const int index = m_PluginsByPriority[priority];
+ const auto& plugin = m_Plugins.at(index);
+
+ if (!plugin->enabled()) {
+ plugin->setIndex(QString());
+ ++numSkipped;
+ continue;
+ }
+
+ if (mediumPluginsAreSupported && plugin->isMediumFile()) {
+ const int ESHpos = 0xFD + (numESHs >> 8);
+ plugin->setIndex((u"%1:%2"_s)
+ .arg(ESHpos, 2, 16, QChar(u'0'))
+ .arg(numESHs & 0xFF, 2, 16, QChar(u'0'))
+ .toUpper());
+ } else if (lightPluginsAreSupported && plugin->isSmallFile()) {
+ const int ESLpos = 0xFE + (numESLs >> 12);
+ plugin->setIndex((u"%1:%2"_s)
+ .arg(ESLpos, 2, 16, QChar(u'0'))
+ .arg(numESLs & 0xFFF, 3, 16, QChar(u'0'))
+ .toUpper());
+ ++numESLs;
+ } else {
+ plugin->setIndex((u"%1"_s).arg(numNormal++, 2, 16, QChar(u'0')).toUpper());
+ }
+ }
+}
+
+void PluginList::refreshLoadOrder()
+{
+ int loadOrder = 0;
+ for (int i = 0; i < m_PluginsByPriority.size(); ++i) {
+ int index = m_PluginsByPriority[i];
+ const auto& plugin = m_Plugins.at(index);
+
+ if (plugin->enabled()) {
+ plugin->setLoadOrder(loadOrder++);
+ } else {
+ plugin->setLoadOrder(-1);
+ }
+ }
+ emit pluginsListChanged();
+}
+
+#pragma endregion Helpers
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/PluginList.h b/libs/installer_bsplugins/src/TESData/PluginList.h
new file mode 100644
index 0000000..b263ffb
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/PluginList.h
@@ -0,0 +1,200 @@
+#ifndef TESDATA_PLUGINLIST_H
+#define TESDATA_PLUGINLIST_H
+
+#include "AssociatedEntry.h"
+#include "FileEntry.h"
+#include "FileInfo.h"
+#include "MOTools/ILootCache.h"
+#include "TESFile/Type.h"
+
+#include <gameplugins.h>
+#include <ifiletree.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <ipluginlist.h>
+#include <memoizedlock.h>
+
+#include <boost/signals2.hpp>
+
+#include <QObject>
+
+#include <atomic>
+#include <map>
+#include <memory>
+#include <set>
+#include <shared_mutex>
+#include <string>
+#include <vector>
+
+namespace TESData
+{
+
+class PluginList final : public QObject,
+ public MOBase::IPluginList,
+ public MOTools::ILootCache
+{
+ Q_OBJECT
+
+public:
+ using SignalRefreshed = boost::signals2::signal<void()>;
+ using SignalPluginMoved = boost::signals2::signal<void(const QString&, int, int)>;
+ using SignalPluginStateChanged =
+ boost::signals2::signal<void(const std::map<QString, PluginStates>&)>;
+
+ explicit PluginList(const MOBase::IOrganizer* moInfo);
+
+ PluginList(const PluginList&) = delete;
+ PluginList(PluginList&&) = delete;
+
+ ~PluginList() noexcept;
+
+ PluginList& operator=(const PluginList&) = delete;
+ PluginList& operator=(PluginList&&) = delete;
+
+ [[nodiscard]] int pluginCount() const;
+
+ [[nodiscard]] const FileInfo* getPlugin(int index) const;
+ [[nodiscard]] const FileInfo* getPluginByName(const QString& name) const;
+ [[nodiscard]] const FileInfo* getPluginByPriority(int priority) const;
+ [[nodiscard]] int getIndex(const QString& pluginName) const;
+ [[nodiscard]] int getIndexAtPriority(int priority) const;
+ [[nodiscard]] QString getOriginName(int index) const;
+
+ [[nodiscard]] FileEntry* findEntryByName(const std::string& pluginName) const;
+ [[nodiscard]] FileEntry* findEntryByHandle(TESFileHandle handle) const;
+ [[nodiscard]] AssociatedEntry* findArchive(const QString& name) const;
+
+ FileEntry* createEntry(const std::string& name);
+ void addRecordConflict(const std::string& pluginName, const RecordPath& path,
+ TESFile::Type type, const std::string& name);
+ void addGroupPlaceholder(const std::string& pluginName, const RecordPath& path);
+
+ void refresh(bool invalidate = false);
+
+ void setEnabled(int id, bool enable);
+ void setEnabled(const std::vector<int>& ids, bool enable);
+ void toggleState(const std::vector<int>& ids);
+
+ [[nodiscard]] bool canMoveToPriority(const std::vector<int>& ids,
+ int newPriority) const;
+ void moveToPriority(std::vector<int> ids, int destination, bool disjoint = false);
+ void shiftPriority(const std::vector<int>& ids, int offset);
+
+ void setGroup(const std::vector<int>& ids, const QString& group);
+
+ [[nodiscard]] QStringList loadOrder() const;
+
+ [[nodiscard]] bool isRefreshing() const { return m_Refreshing; }
+
+ void notifyPendingState(const QString& mod, MOBase::IModList::ModStates state);
+ void flushPendingStates();
+
+ // IPluginList
+
+ [[nodiscard]] QStringList pluginNames() const override;
+ [[nodiscard]] PluginStates state(const QString& name) const override;
+ void setState(const QString& name, PluginStates state) override;
+ [[nodiscard]] int priority(const QString& name) const override;
+ bool setPriority(const QString& name, int newPriority) override;
+ [[nodiscard]] int loadOrder(const QString& name) const override;
+ void setLoadOrder(const QStringList& pluginList) override;
+ [[deprecated]] bool isMaster(const QString& name) const override;
+ [[nodiscard]] QStringList masters(const QString& name) const override;
+ [[nodiscard]] QString origin(const QString& name) const override;
+
+ bool onRefreshed(const std::function<void()>& callback) override;
+ bool
+ onPluginMoved(const std::function<void(const QString&, int, int)>& func) override;
+ bool onPluginStateChanged(
+ const std::function<void(const std::map<QString, PluginStates>&)>& func) override;
+
+ [[nodiscard]] bool hasMasterExtension(const QString& name) const override;
+ [[nodiscard]] bool hasLightExtension(const QString& name) const override;
+ [[nodiscard]] bool isMasterFlagged(const QString& name) const override;
+ [[nodiscard]] bool isMediumFlagged(const QString& name) const override;
+ [[nodiscard]] bool isLightFlagged(const QString& name) const override;
+ [[nodiscard]] bool isBlueprintFlagged(const QString& name) const override;
+ [[nodiscard]] bool hasNoRecords(const QString& name) const override;
+ [[nodiscard]] int formVersion(const QString& name) const override;
+ [[nodiscard]] float headerVersion(const QString& name) const override;
+
+ [[nodiscard]] QString author(const QString& name) const override;
+ [[nodiscard]] QString description(const QString& name) const override;
+
+ // ILootCache
+
+ void clearAdditionalInformation() override;
+ void addLootReport(const QString& name, MOTools::Loot::Plugin plugin) override;
+ [[nodiscard]] const MOTools::Loot::Plugin* getLootReport(const QString& name) const;
+
+public slots:
+ void writePluginLists() const;
+
+signals:
+ void pluginsListChanged();
+
+private:
+ [[nodiscard]] FileInfo* findPlugin(const QString& name);
+ [[nodiscard]] const FileInfo* findPlugin(const QString& name) const;
+
+ void scanDataFiles(bool invalidate);
+ void readPluginLists();
+ void checkBsa(TESData::FileInfo& info,
+ const std::shared_ptr<const MOBase::IFileTree>& fileTree);
+ void associateArchive(const TESData::FileInfo& info, const QString& archiveName);
+
+ [[nodiscard]] QString groupsPath() const;
+ [[nodiscard]] QString lockedOrderPath() const;
+ void clearGroups();
+ void readGroups(const QString& fileName);
+ void writeEmptyTextFile(const QString& fileName) const;
+ void writeGroups(const QString& fileName) const;
+ [[nodiscard]] QString destinationGroup(
+ int oldPriority, int newPriority, const QString& originalGroup, bool isESM,
+ const boost::container::flat_set<QString, MOBase::FileNameComparator>&
+ exclusions);
+
+ void queuePluginStateChange(const QString& pluginName, PluginStates state);
+ void dispatchPluginStateChanges();
+ void pluginStatesChanged(const QStringList& pluginNames, PluginStates state) const;
+ void enforcePluginRelationships();
+ void testMasters();
+ void updateCache();
+ void computeCompileIndices();
+ void refreshLoadOrder();
+
+ const MOBase::IOrganizer* m_Organizer;
+
+ std::vector<std::shared_ptr<FileInfo>> m_Plugins;
+
+ std::map<QString, int, MOBase::FileNameComparator> m_PluginsByName;
+ std::vector<int> m_PluginsByPriority;
+
+ std::map<QString, MOTools::Loot::Plugin, MOBase::FileNameComparator> m_LootInfo;
+
+ std::atomic<TESFileHandle> m_NextHandle = 0;
+ std::map<std::string, std::shared_ptr<FileEntry>, TESFile::less> m_EntriesByName;
+ boost::container::flat_map<TESFileHandle, std::shared_ptr<FileEntry>>
+ m_EntriesByHandle;
+ std::map<std::string, std::shared_ptr<Record>> m_Settings;
+ std::map<TESFile::Type, std::shared_ptr<Record>> m_DefaultObjects;
+
+ std::shared_ptr<AssociatedEntry> m_MasterArchiveEntry;
+ std::map<QString, std::shared_ptr<AssociatedEntry>, MOBase::FileNameComparator>
+ m_Archives;
+
+ mutable std::shared_mutex m_FileEntryMutex;
+ mutable std::shared_mutex m_ArchiveEntryMutex;
+
+ bool m_Refreshing = true;
+ std::map<QString, PluginStates> m_QueuedStateChanges;
+ std::set<QString> m_PendingActive;
+
+ SignalRefreshed m_Refreshed;
+ SignalPluginMoved m_PluginMoved;
+ SignalPluginStateChanged m_PluginStateChanged;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_PLUGINLIST_H
diff --git a/libs/installer_bsplugins/src/TESData/Record.h b/libs/installer_bsplugins/src/TESData/Record.h
new file mode 100644
index 0000000..0edda85
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/Record.h
@@ -0,0 +1,90 @@
+#ifndef TESDATA_RECORD_H
+#define TESDATA_RECORD_H
+
+#include "TESFile/Type.h"
+
+#include <QString>
+
+#include <set>
+#include <span>
+
+namespace TESData
+{
+
+using TESFileHandle = int;
+
+class Record final
+{
+public:
+ using Identifier =
+ std::variant<std::monostate, std::uint32_t, std::string, TESFile::Type>;
+
+ [[nodiscard]] TESFile::Type formType() const { return m_FormType; }
+
+ [[nodiscard]] const std::string& file() const { return m_File; }
+
+ [[nodiscard]] const Identifier& identifier() const { return m_Identifier; }
+
+ [[nodiscard]] bool hasFormId() const
+ {
+ return std::holds_alternative<std::uint32_t>(m_Identifier);
+ }
+
+ [[nodiscard]] bool hasEditorId() const
+ {
+ return std::holds_alternative<std::string>(m_Identifier);
+ }
+
+ [[nodiscard]] bool hasTypeId() const
+ {
+ return std::holds_alternative<TESFile::Type>(m_Identifier);
+ }
+
+ [[nodiscard]] std::uint32_t formId() const
+ {
+ return std::get<std::uint32_t>(m_Identifier);
+ }
+
+ [[nodiscard]] const std::string& editorId() const
+ {
+ return std::get<std::string>(m_Identifier);
+ }
+
+ [[nodiscard]] TESFile::Type typeId() const
+ {
+ return std::get<TESFile::Type>(m_Identifier);
+ }
+
+ [[nodiscard]] const std::set<TESFileHandle>& alternatives() const
+ {
+ return m_Alternatives;
+ }
+
+ [[nodiscard]] bool ignored() const { return m_Ignored; }
+ void setIgnored(bool value) { m_Ignored = value; }
+
+ void setIdentifier(const Identifier& identifier, std::span<const std::string> files)
+ {
+ m_Identifier = identifier;
+
+ if (std::holds_alternative<std::uint32_t>(m_Identifier)) {
+ std::uint32_t& formId = std::get<std::uint32_t>(m_Identifier);
+ const std::uint8_t localIndex = formId >> 24U;
+ formId &= ~0xFF000000U;
+ m_File = files[localIndex];
+ }
+ }
+
+ void addAlternative(TESFileHandle origin) { m_Alternatives.insert(origin); }
+
+private:
+ TESFile::Type m_FormType;
+ std::string m_File;
+ Identifier m_Identifier;
+ std::set<TESFileHandle> m_Alternatives;
+ bool m_Ignored = false;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_RECORD_H
diff --git a/libs/installer_bsplugins/src/TESData/RecordPath.cpp b/libs/installer_bsplugins/src/TESData/RecordPath.cpp
new file mode 100644
index 0000000..ac6a575
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/RecordPath.cpp
@@ -0,0 +1,158 @@
+#include "RecordPath.h"
+
+#include <algorithm>
+#include <iomanip>
+#include <iterator>
+#include <sstream>
+
+namespace TESData
+{
+
+std::string RecordPath::string() const
+{
+ std::ostringstream ss;
+ ss << ":/";
+
+ for (int i = 0; i < m_Groups.size(); ++i) {
+ const TESFile::GroupData group = m_Groups[i];
+
+ if (group.hasFormType()) {
+ ss << group.formType().view() << "/";
+ } else if (group.hasParent()) {
+ if (i == 0 || !m_Groups[i - 1].hasParent() ||
+ m_Groups[i - 1].parent() != group.parent()) {
+ const auto parentId = group.parent();
+ const std::uint8_t localIndex = parentId >> 24U;
+ const std::string& owner = m_Files.at(localIndex);
+ ss << owner << "|";
+ ss << std::hex << std::setfill('0') << std::setw(6) << (parentId & 0xFFFFFF);
+ ss << "/";
+ } else {
+ switch (group.type()) {
+ case TESFile::GroupType::CellPersistentChildren:
+ ss << "Persistent/";
+ break;
+ case TESFile::GroupType::CellTemporaryChildren:
+ ss << "Temporary/";
+ break;
+ case TESFile::GroupType::CellVisibleDistantChildren:
+ ss << "Visible Distant/";
+ break;
+ }
+ }
+ } else if (group.hasBlock()) {
+ ss << group.block() << "/";
+ } else if (group.hasGridCell()) {
+ auto [x, y] = group.gridCell();
+ ss << x << ", " << y << "/";
+ }
+ }
+
+ if (hasFormId()) {
+ const auto id = formId();
+ const std::uint8_t localIndex = id >> 24U;
+ const std::string& owner = m_Files.at(localIndex);
+ ss << owner << "|";
+ ss << std::hex << std::setfill('0') << std::setw(6) << (id & 0xFFFFFF);
+ } else if (hasEditorId()) {
+ ss << editorId();
+ } else if (hasTypeId()) {
+ ss << typeId();
+ }
+
+ return ss.str();
+}
+
+void RecordPath::setFormId(std::uint32_t formId, std::span<const std::string> masters,
+ const std::string& file)
+{
+ const std::uint8_t localIndex = formId >> 24U;
+ const std::string& owner = localIndex < masters.size() ? masters[localIndex] : file;
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(m_Files), TESFile::find(m_Files, owner)));
+
+ if (newIndex == m_Files.size()) {
+ m_Files.push_back(owner);
+ }
+
+ m_Identifier = (formId & 0xFFFFFF) | (newIndex << 24U);
+}
+
+void RecordPath::unsetFormId()
+{
+ if (!hasFormId())
+ return;
+
+ const std::uint8_t localIndex = formId() >> 24U;
+ m_Identifier = {};
+
+ if (localIndex == m_Files.size() - 1) {
+ cleanLastFile();
+ }
+}
+
+void RecordPath::setEditorId(const std::string& editorId)
+{
+ unsetFormId();
+ m_Identifier = editorId;
+}
+
+void RecordPath::setTypeId(TESFile::Type type)
+{
+ unsetFormId();
+ m_Identifier = type;
+}
+
+void RecordPath::setIdentifier(const Identifier& identifier,
+ std::span<const std::string> masters)
+{
+ if (std::holds_alternative<std::uint32_t>(identifier)) {
+ setFormId(std::get<std::uint32_t>(identifier), masters, "");
+ } else {
+ unsetFormId();
+ m_Identifier = identifier;
+ }
+}
+
+void RecordPath::push(TESFile::GroupData group, std::span<const std::string> masters,
+ const std::string& file)
+{
+ if (group.hasParent()) {
+ const std::uint8_t localIndex = group.parent() >> 24U;
+ const std::string& owner = localIndex < masters.size() ? masters[localIndex] : file;
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(m_Files), TESFile::find(m_Files, owner)));
+
+ if (newIndex == m_Files.size()) {
+ m_Files.push_back(owner);
+ }
+
+ group.setLocalIndex(newIndex);
+ }
+
+ m_Groups.push_back(group);
+}
+
+void RecordPath::pop()
+{
+ unsetFormId();
+
+ const auto& top = m_Groups.back();
+ const std::uint8_t localIndex = top.hasParent() ? top.parent() >> 24U : 0xFF;
+ m_Groups.pop_back();
+
+ if (localIndex == m_Files.size() - 1) {
+ cleanLastFile();
+ }
+}
+
+void RecordPath::cleanLastFile()
+{
+ if (!std::ranges::any_of(m_Groups, [&](auto&& group) {
+ return group.hasParent() && group.parent() >> 24U == m_Files.size() - 1;
+ })) {
+ m_Files.pop_back();
+ }
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/RecordPath.h b/libs/installer_bsplugins/src/TESData/RecordPath.h
new file mode 100644
index 0000000..712edfa
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/RecordPath.h
@@ -0,0 +1,86 @@
+#ifndef TESDATA_RECORDPATH_H
+#define TESDATA_RECORDPATH_H
+
+#include "TESFile/Stream.h"
+
+#include <boost/container/small_vector.hpp>
+
+#include <cstdint>
+#include <span>
+#include <string>
+#include <variant>
+
+namespace TESData
+{
+
+class RecordPath final
+{
+public:
+ using Identifier =
+ std::variant<std::monostate, std::uint32_t, std::string, TESFile::Type>;
+
+ [[nodiscard]] bool hasFormId() const
+ {
+ return std::holds_alternative<std::uint32_t>(m_Identifier);
+ }
+
+ [[nodiscard]] bool hasEditorId() const
+ {
+ return std::holds_alternative<std::string>(m_Identifier);
+ }
+
+ [[nodiscard]] bool hasTypeId() const
+ {
+ return std::holds_alternative<TESFile::Type>(m_Identifier);
+ }
+
+ [[nodiscard]] std::uint32_t formId() const
+ {
+ return std::get<std::uint32_t>(m_Identifier);
+ }
+
+ [[nodiscard]] const std::string& editorId() const
+ {
+ return std::get<std::string>(m_Identifier);
+ }
+
+ [[nodiscard]] TESFile::Type typeId() const
+ {
+ return std::get<TESFile::Type>(m_Identifier);
+ }
+
+ [[nodiscard]] std::span<const std::string> files() const { return m_Files; }
+
+ [[nodiscard]] std::span<const TESFile::GroupData> groups() const { return m_Groups; }
+
+ [[nodiscard]] const auto& identifier() const { return m_Identifier; }
+
+ [[nodiscard]] std::string string() const;
+
+ void setFormId(std::uint32_t formId, std::span<const std::string> masters,
+ const std::string& file);
+
+ void unsetFormId();
+
+ void setEditorId(const std::string& editorId);
+
+ void setTypeId(TESFile::Type type);
+
+ void setIdentifier(const Identifier& identifier, std::span<const std::string> files);
+
+ void push(TESFile::GroupData group, const std::span<const std::string> masters,
+ const std::string& file);
+
+ void pop();
+
+private:
+ void cleanLastFile();
+
+ boost::container::small_vector<std::string, 2> m_Files;
+ boost::container::small_vector<TESFile::GroupData, 4> m_Groups;
+ Identifier m_Identifier;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_RECORDPATH_H
diff --git a/libs/installer_bsplugins/src/TESData/SingleRecordParser.cpp b/libs/installer_bsplugins/src/TESData/SingleRecordParser.cpp
new file mode 100644
index 0000000..5cc553d
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/SingleRecordParser.cpp
@@ -0,0 +1,179 @@
+#include "SingleRecordParser.h"
+#include "FormParser.h"
+
+#include <algorithm>
+#include <array>
+#include <iterator>
+#include <utility>
+
+using namespace Qt::Literals::StringLiterals;
+
+namespace TESData
+{
+
+using Game = FormParserManager::Game;
+
+static constexpr auto GameMap = std::to_array<std::pair<QStringView, Game>>({
+ {u"Skyrim Special Edition", Game::SSE},
+ {u"Skyrim VR", Game::SSE},
+ {u"Enderal Special Edition", Game::SSE},
+ {u"Fallout 4", Game::FO4},
+ {u"Fallout 4 VR", Game::FO4},
+ {u"Skyrim", Game::TES5},
+ {u"Enderal", Game::TES5},
+ {u"New Vegas", Game::FNV},
+ {u"TTW", Game::FNV},
+ {u"Fallout 3", Game::FO3},
+ {u"Oblivion", Game::TES4},
+ {u"Oblivion Remastered", Game::TES4},
+});
+
+static Game gameIdentifier(QStringView gameName)
+{
+ const auto it = std::ranges::find_if(GameMap, [=](auto&& pair) {
+ return pair.first == gameName;
+ });
+
+ if (it != std::end(GameMap)) {
+ return it->second;
+ } else {
+ return Game::Unknown;
+ }
+}
+
+SingleRecordParser::SingleRecordParser(const QString& gameName, const RecordPath& path,
+ const std::string& file, DataItem* root,
+ int index)
+ : m_GameName{gameName}, m_Path{path}, m_File{file}, m_DataRoot{root},
+ m_FileIndex{index}
+{}
+
+bool SingleRecordParser::Group(TESFile::GroupData group)
+{
+ if (m_Depth == m_Path.groups().size()) {
+ return false;
+ }
+
+ if (group.hasParent()) {
+ const std::uint8_t localIndex = group.parent() >> 24U;
+ const std::string& owner =
+ localIndex < m_Masters.size() ? m_Masters[localIndex] : m_File;
+ const auto files = m_Path.files();
+ const std::uint8_t newIndex = static_cast<std::uint8_t>(
+ std::distance(std::begin(files), TESFile::find(files, owner)));
+
+ group.setLocalIndex(newIndex);
+ }
+
+ if (group == m_Path.groups()[m_Depth]) {
+ ++m_Depth;
+ return true;
+ }
+
+ return false;
+}
+
+bool SingleRecordParser::Form(TESFile::FormData form)
+{
+ m_CurrentType = form.type();
+ m_CurrentFlags = form.flags();
+
+ if (m_Depth == 0) {
+ if (form.type() == "TES4"_ts) {
+ m_Localized = (form.flags() & TESFile::RecordFlags::Localized);
+ }
+
+ return true;
+ }
+
+ if (m_Path.hasFormId()) {
+ if ((form.formId() & 0xFFFFFF) != (m_Path.formId() & 0xFFFFFF)) {
+ return false;
+ }
+
+ const std::uint8_t localIndex = form.formId() >> 24U;
+ const std::string& owner =
+ localIndex < m_Masters.size() ? m_Masters[localIndex] : m_File;
+ if (!TESFile::iequals(m_Path.files()[m_Path.formId() >> 24U], owner)) {
+ return false;
+ }
+
+ m_RecordFound = true;
+ const auto game = gameIdentifier(m_GameName);
+ FormParserManager::getParser(game, m_CurrentType)
+ ->parseFlags(m_DataRoot, m_FileIndex, m_CurrentFlags);
+ return true;
+ } else {
+ return !m_RecordFound;
+ }
+
+ return false;
+}
+
+bool SingleRecordParser::Chunk(TESFile::Type type)
+{
+ m_CurrentChunk = type;
+ return true;
+}
+
+void SingleRecordParser::Data(std::istream& stream)
+{
+ m_ChunkStream = &stream;
+
+ if (m_Depth == 0) {
+ if (m_CurrentChunk == "MAST"_ts) {
+ const std::string master = TESFile::readZstring(stream);
+ if (!master.empty()) {
+ m_Masters.push_back(master);
+ }
+ }
+ return;
+ }
+
+ if (m_Path.hasEditorId() && m_CurrentChunk == "EDID"_ts) {
+ const std::string editorId = TESFile::readZstring(stream);
+ if (editorId != m_Path.editorId()) {
+ return;
+ }
+
+ m_RecordFound = true;
+ const auto game = gameIdentifier(m_GameName);
+ FormParserManager::getParser(game, m_CurrentType)
+ ->parseFlags(m_DataRoot, m_FileIndex, m_CurrentFlags);
+ stream.seekg(std::ios::beg);
+ }
+
+ if (m_Path.hasTypeId() && m_CurrentChunk == "DNAM"_ts) {
+ while (stream.peek() != std::char_traits<char>::eof()) {
+ const TESFile::Type name = TESFile::readType<TESFile::Type>(stream);
+ const QString formIdString = readFormId(m_Masters, m_File, stream);
+ if (name == m_Path.typeId()) {
+ m_DataRoot->getOrInsertChild(0, name, u""_s)
+ ->setData(m_FileIndex, formIdString);
+ m_RecordFound = true;
+ break;
+ }
+ if (name == "BBBB"_ts) {
+ break;
+ }
+ }
+ return;
+ }
+
+ if (!m_RecordFound) {
+ return;
+ }
+
+ if (!m_ParseTask) {
+ const auto game = gameIdentifier(m_GameName);
+ m_ParseTask = FormParserManager::getParser(game, m_CurrentType)
+ ->parseForm(m_DataRoot, m_FileIndex, m_Localized, m_Masters,
+ m_File, m_CurrentChunk, m_ChunkStream);
+ }
+
+ if (!m_ParseTask.done()) {
+ m_ParseTask.resume();
+ }
+}
+
+} // namespace TESData
diff --git a/libs/installer_bsplugins/src/TESData/SingleRecordParser.h b/libs/installer_bsplugins/src/TESData/SingleRecordParser.h
new file mode 100644
index 0000000..4304166
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/SingleRecordParser.h
@@ -0,0 +1,52 @@
+#ifndef TESDATA_SINGLERECORDPARSER_H
+#define TESDATA_SINGLERECORDPARSER_H
+
+#include "DataItem.h"
+#include "RecordPath.h"
+#include "TESFile/Stream.h"
+
+#include <iplugingame.h>
+
+#include <coroutine>
+#include <functional>
+#include <memory>
+#include <vector>
+
+namespace TESData
+{
+
+class IFormParser;
+
+class SingleRecordParser final
+{
+public:
+ SingleRecordParser(const QString& gameName, const RecordPath& path,
+ const std::string& file, DataItem* root, int index);
+
+ bool Group(TESFile::GroupData group);
+ bool Form(TESFile::FormData form);
+ bool Chunk(TESFile::Type type);
+ void Data(std::istream& stream);
+
+private:
+ QString m_GameName;
+ RecordPath m_Path;
+ std::string m_File;
+ DataItem* m_DataRoot;
+ int m_FileIndex;
+
+ TESFile::Type m_CurrentType;
+ std::uint32_t m_CurrentFlags = 0;
+ std::coroutine_handle<> m_ParseTask;
+ std::istream* m_ChunkStream;
+
+ std::vector<std::string> m_Masters;
+ int m_Depth = 0;
+ bool m_Localized = false;
+ bool m_RecordFound = false;
+ TESFile::Type m_CurrentChunk;
+};
+
+} // namespace TESData
+
+#endif // TESDATA_SINGLERECORDPARSER_H
diff --git a/libs/installer_bsplugins/src/TESData/TypeStringNames.h b/libs/installer_bsplugins/src/TESData/TypeStringNames.h
new file mode 100644
index 0000000..61f6916
--- /dev/null
+++ b/libs/installer_bsplugins/src/TESData/TypeStringNames.h
@@ -0,0 +1,560 @@
+#ifndef TESDATA_FORMNAMES_H
+#define TESDATA_FORMNAMES_H
+
+#include "TESFile/Type.h"
+
+#include <QStringView>
+
+#include <algorithm>
+#include <array>
+#include <utility>
+
+namespace TESData
+{
+
+constexpr auto FormNames = std::to_array<std::pair<TESFile::Type, QStringView>>({
+ {"AACT"_ts, u"Actor Action"},
+ {"ACHR"_ts, u"Placed NPC"},
+ {"ACTI"_ts, u"Activator"},
+ {"ADDN"_ts, u"AddOnNode"},
+ {"ALCH"_ts, u"Potion"},
+ {"AMMO"_ts, u"Ammo"},
+ {"ANIO"_ts, u"AnimObject"},
+ {"APPA"_ts, u"Alchemical Apparatus"},
+ {"ARMA"_ts, u"ArmorAddon"},
+ {"ARMO"_ts, u"Armor"},
+ {"ARTO"_ts, u"Art Object"},
+ {"ASPC"_ts, u"Acoustic Space"},
+ {"ASTP"_ts, u"Association Type"},
+ {"AVIF"_ts, u"ActorValueInfo"},
+ {"BOOK"_ts, u"Book"},
+ {"BPTD"_ts, u"BodyPartData"},
+ {"CAMS"_ts, u"CameraShot"},
+ {"CELL"_ts, u"Cell"},
+ {"CLAS"_ts, u"Class"},
+ {"CLDC"_ts, u""},
+ {"CLFM"_ts, u"ColorForm"},
+ {"CLMT"_ts, u"Climate"},
+ {"COBJ"_ts, u"Constructible Object"},
+ {"COLL"_ts, u"Collision Layer"},
+ {"CONT"_ts, u"Container"},
+ {"CPTH"_ts, u"Camera Path"},
+ {"CSTY"_ts, u"CombatStyle"},
+ {"DEBR"_ts, u"Debris"},
+ {"DIAL"_ts, u"Dialogue Topic"},
+ {"DLBR"_ts, u"Dialogue Branch"},
+ {"DLVW"_ts, u"Dialogue View"},
+ {"DOBJ"_ts, u"Default Object"},
+ {"DOOR"_ts, u"Door"},
+ {"DUAL"_ts, u"Dual Cast Data"},
+ {"ECZN"_ts, u"Encounter Zone"},
+ {"EFSH"_ts, u"EffectShader"},
+ {"ENCH"_ts, u"Enchantment"},
+ {"EQUP"_ts, u"Equip Slot"},
+ {"EXPL"_ts, u"Explosion"},
+ {"EYES"_ts, u"Eyes"},
+ {"FACT"_ts, u"Faction"},
+ {"FLOR"_ts, u"Flora"},
+ {"FLST"_ts, u"FormList"},
+ {"FSTP"_ts, u"Footstep"},
+ {"FSTS"_ts, u"Footstep Set"},
+ {"FURN"_ts, u"Furniture"},
+ {"GLOB"_ts, u"Global"},
+ {"GMST"_ts, u"GameSetting"},
+ {"GRAS"_ts, u"Grass"},
+ {"HAIR"_ts, u""},
+ {"HAZD"_ts, u"Hazard"},
+ {"HDPT"_ts, u"HeadPart"},
+ {"IDLE"_ts, u"Animation"},
+ {"IDLM"_ts, u"IdleMarker"},
+ {"IMAD"_ts, u"Imagespace Modifier"},
+ {"IMGS"_ts, u"Imagespace"},
+ {"INFO"_ts, u"Topic Info"},
+ {"INGR"_ts, u"Ingredient"},
+ {"IPCT"_ts, u"ImpactData"},
+ {"IPDS"_ts, u"ImpactDataSet"},
+ {"KEYM"_ts, u"Key"},
+ {"KYWD"_ts, u"Keyword"},
+ {"LAND"_ts, u"Landscape"},
+ {"LCRT"_ts, u"Location Ref Type"},
+ {"LCTN"_ts, u"Location"},
+ {"LENS"_ts, u"LensFlare"},
+ {"LGTM"_ts, u"Lighting Template"},
+ {"LIGH"_ts, u"Light"},
+ {"LSCR"_ts, u"LoadScreen"},
+ {"LTEX"_ts, u"LandTexture"},
+ {"LVLI"_ts, u"LeveledItem"},
+ {"LVLN"_ts, u"LeveledCharacter"},
+ {"LVSP"_ts, u"LeveledSpell"},
+ {"MATO"_ts, u"Material Object"},
+ {"MATT"_ts, u"Material Type"},
+ {"MESG"_ts, u"Message"},
+ {"MGEF"_ts, u"Magic Effect"},
+ {"MISC"_ts, u"MiscItem"},
+ {"MOVT"_ts, u"Movement Type"},
+ {"MSTT"_ts, u"MovableStatic"},
+ {"MUSC"_ts, u"Music Type"},
+ {"MUST"_ts, u"Music Track"},
+ {"NAVI"_ts, u"Navigation Mesh Info Map"},
+ {"NAVM"_ts, u"Navigation Mesh"},
+ {"NPC_"_ts, u"Actor"},
+ {"OTFT"_ts, u"Outfit"},
+ {"PACK"_ts, u"Package"},
+ {"PARW"_ts, u"Placed Arrow"},
+ {"PBAR"_ts, u"Placed Barrier"},
+ {"PBEA"_ts, u"Placed Beam"},
+ {"PCON"_ts, u"Placed Cone/Voice"},
+ {"PERK"_ts, u"Perk"},
+ {"PFLA"_ts, u"Placed Flame"},
+ {"PGRE"_ts, u"Placed Projectile"},
+ {"PHZD"_ts, u"Placed Hazard"},
+ {"PLYR"_ts, u"Player Reference"},
+ {"PMIS"_ts, u"Placed Missile"},
+ {"PROJ"_ts, u"Projectile"},
+ {"PWAT"_ts, u""},
+ {"QUST"_ts, u"Quest"},
+ {"RACE"_ts, u"Race"},
+ {"REFR"_ts, u"Placed Object"},
+ {"REGN"_ts, u"Region"},
+ {"RELA"_ts, u"Relationship"},
+ {"REVB"_ts, u"Reverb Parameters"},
+ {"RFCT"_ts, u"Visual Effect"},
+ {"RGDL"_ts, u""},
+ {"SCEN"_ts, u"Scene"},
+ {"SCOL"_ts, u"Static Collection"},
+ {"SCPT"_ts, u""},
+ {"SCRL"_ts, u"Scroll"},
+ {"SHOU"_ts, u"Shout"},
+ {"SLGM"_ts, u"Soul Gem"},
+ {"SMBN"_ts, u"Story Manager Branch Node"},
+ {"SMEN"_ts, u"Story Manager Event Node"},
+ {"SMQN"_ts, u"Story Manager Quest Node"},
+ {"SNCT"_ts, u"Sound Category"},
+ {"SNDR"_ts, u"Sound Descriptor"},
+ {"SOPM"_ts, u"Sound Output Model"},
+ {"SOUN"_ts, u"Sound Marker"},
+ {"SPEL"_ts, u"Spell"},
+ {"SPGD"_ts, u"Shader Particle Geometry"},
+ {"STAT"_ts, u"Static"},
+ {"TACT"_ts, u"TalkingActivator"},
+ {"TES4"_ts, u"File Header"},
+ {"TREE"_ts, u"Tree"},
+ {"TXST"_ts, u"TextureSet"},
+ {"VOLI"_ts, u"VolumetricLighting"},
+ {"VTYP"_ts, u"VoiceType"},
+ {"WATR"_ts, u"WaterType"},
+ {"WEAP"_ts, u"Weapon"},
+ {"WOOP"_ts, u"Word of Power"},
+ {"WRLD"_ts, u"World Space"},
+ {"WTHR"_ts, u"Weather"},
+});
+static_assert(std::ranges::is_sorted(FormNames));
+
+constexpr auto DefaultObjectNames =
+ std::to_array<std::pair<TESFile::Type, QStringView>>({
+ {"AAAC"_ts, u"Action Activate"},
+ {"AAB1"_ts, u"Action Bleedout Start"},
+ {"AAB2"_ts, u"Action Bleedout Stop"},
+ {"AABA"_ts, u"Action Block Anticipate"},
+ {"AABH"_ts, u"Action Block Hit"},
+ {"AABI"_ts, u"Action Bumped Into"},
+ {"AADA"_ts, u"Action DualAttack"},
+ {"AADE"_ts, u"Action Death"},
+ {"AADL"_ts, u"Action DualRelease"},
+ {"AADR"_ts, u"Action Draw"},
+ {"AADW"_ts, u"Action Death Wait"},
+ {"AAF1"_ts, u"Action Fly Start"},
+ {"AAF2"_ts, u"Action Fly Stop"},
+ {"AAFA"_ts, u"Action Fall"},
+ {"AAFQ"_ts, u"Action Force Equip"},
+ {"AAGU"_ts, u"Action Get Up"},
+ {"AAH1"_ts, u"Action Hover Start"},
+ {"AAH2"_ts, u"Action Hover Stop"},
+ {"AAID"_ts, u"Action Idle"},
+ {"AAIS"_ts, u"Action Idle Stop"},
+ {"AAJP"_ts, u"Action Jump"},
+ {"AALA"_ts, u"Action LeftAttack"},
+ {"AALD"_ts, u"Action LeftReady"},
+ {"AALI"_ts, u"Action LeftInterrupt"},
+ {"AALK"_ts, u"Action Look"},
+ {"AALM"_ts, u"Action Large Movement Delta"},
+ {"AALN"_ts, u"Action Land"},
+ {"AALR"_ts, u"Action LeftRelease"},
+ {"AAPA"_ts, u"Action Right Power Attack"},
+ {"AAPE"_ts, u"Action Path End"},
+ {"AAPS"_ts, u"Action Path Start"},
+ {"AAR2"_ts, u"Action Large Recoil"},
+ {"AARA"_ts, u"Action RightAttack"},
+ {"AARC"_ts, u"Action Recoil"},
+ {"AARD"_ts, u"Action RightReady"},
+ {"AARI"_ts, u"Action RightInterrupt"},
+ {"AARR"_ts, u"Action RightRelease"},
+ {"AAS1"_ts, u"Action Stagger Start"},
+ {"AASC"_ts, u"Action Shield Change"},
+ {"AASH"_ts, u"Action Sheath"},
+ {"AASN"_ts, u"Action Sneak"},
+ {"AASP"_ts, u"Action Sprint Stop"},
+ {"AASS"_ts, u"Action Summoned Start"},
+ {"AAST"_ts, u"Action Sprint Start"},
+ {"AASW"_ts, u"Action Swim State Change"},
+ {"AAVC"_ts, u"Action Voice"},
+ {"AAVD"_ts, u"Action VoiceReady"},
+ {"AAVI"_ts, u"Action VoiceInterrupt"},
+ {"AAVR"_ts, u"Action VoiceRelease"},
+ {"AAWH"_ts, u"Action Ward Hit"},
+ {"ABSE"_ts, u"Art Object - Absorb Effect"},
+ {"ADPA"_ts, u"Action Dual Power Attack"},
+ {"AFNP"_ts, u"Keyword - Activator Furniture No Player"},
+ {"AHBM"_ts, u"Keyword - Armor Material Heavy Bonemold"},
+ {"AHCH"_ts, u"Keyword - Armor Material Heavy Chitin"},
+ {"AHNC"_ts, u"Keyword - Armor Material Heavy Nordic"},
+ {"AHSM"_ts, u"Keyword - Armor Material Heavy Stalhrim"},
+ {"AIDW"_ts, u"Action Idle Warn"},
+ {"AIVC"_ts, u"Verlet Cape"},
+ {"AKDN"_ts, u"Action Knockdown"},
+ {"ALBM"_ts, u"Keyword - Armor Material Light Bonemold"},
+ {"ALCH"_ts, u"Keyword - Armor Material Light Chitin"},
+ {"ALDM"_ts, u"Ash LOD Material"},
+ {"ALHD"_ts, u"Ash LOD Material (HD)"},
+ {"ALNC"_ts, u"Keyword - Armor Material Light Nordic"},
+ {"ALPA"_ts, u"Action Left Power Attack"},
+ {"ALSM"_ts, u"Keyword - Armor Material Light Stalhrim"},
+ {"ALTI"_ts, u"Action Listen Idle"},
+ {"AMBK"_ts, u"Action Move Backward"},
+ {"AMFD"_ts, u"Action Move Forward"},
+ {"AMLT"_ts, u"Action Move Left"},
+ {"AMRT"_ts, u"Action Move Right"},
+ {"AMSP"_ts, u"Action Move Stop"},
+ {"AMST"_ts, u"Action Move Start"},
+ {"ANML"_ts, u"Keyword - Animal"},
+ {"AODA"_ts, u"Keyword - Armor Material Daedric"},
+ {"AODB"_ts, u"Keyword - Armor Material Dragonbone"},
+ {"AODP"_ts, u"Keyword - Armor Material Dragonplate"},
+ {"AODS"_ts, u"Keyword - Armor Material Dragonscale"},
+ {"AODW"_ts, u"Keyword - Armor Material Dwarven"},
+ {"AOEB"_ts, u"Keyword - Armor Material Ebony"},
+ {"AOEL"_ts, u"Keyword - Armor Material Elven"},
+ {"AOES"_ts, u"Keyword - Armor Material ElvenSplinted"},
+ {"AOFE"_ts, u"Keyword - Armor Material Iron"},
+ {"AOFL"_ts, u"Keyword - Armor Material FullLeather"},
+ {"AOGL"_ts, u"Keyword - Armor Material Glass"},
+ {"AOHI"_ts, u"Keyword - Armor Material Hide"},
+ {"AOIB"_ts, u"Keyword - Armor Material IronBanded"},
+ {"AOIH"_ts, u"Keyword - Armor Material ImperialHeavy"},
+ {"AOIM"_ts, u"Keyword - Armor Material Imperial"},
+ {"AOIR"_ts, u"Keyword - Armor Material ImperialReinforced"},
+ {"AOOR"_ts, u"Keyword - Armor Material Orcish"},
+ {"AOSC"_ts, u"Keyword - Armor Material Scaled"},
+ {"AOSD"_ts, u"Keyword - Armor Material Studded"},
+ {"AOSK"_ts, u"Keyword - Armor Material Stormcloak"},
+ {"AOSP"_ts, u"Keyword - Armor Material SteelPlate"},
+ {"AOST"_ts, u"Keyword - Armor Material Steel"},
+ {"APSH"_ts, u"Allow Player Shout"},
+ {"ARAG"_ts, u"Action Reset Animation Graph"},
+ {"AREL"_ts, u"Action Reload"},
+ {"ARGI"_ts, u"Action Ragdoll Instant"},
+ {"ARTL"_ts, u"Armor Material List"},
+ {"ASID"_ts, u"Action Idle Stop Instant"},
+ {"ATKI"_ts, u"Action Talking Idle"},
+ {"ATLE"_ts, u"Action Turn Left"},
+ {"ATRI"_ts, u"Action Turn Right"},
+ {"ATSP"_ts, u"Action Turn Stop"},
+ {"AVVP"_ts, u"Vampire Available Perks"},
+ {"AVWP"_ts, u"Werewolf Available Perks"},
+ {"AWWS"_ts, u"Action Waterwalk Start"},
+ {"AWWW"_ts, u"Bunny Faction"},
+ {"BAPO"_ts, u"Base Potion"},
+ {"BAPS"_ts, u"Base Poison"},
+ {"BEEP"_ts, u"Keyword - Robot"},
+ {"BENA"_ts, u"Base Armor Enchantment"},
+ {"BENW"_ts, u"Base Weapon Enchantment"},
+ {"BTMS"_ts, u"Battle Music"},
+ {"CACA"_ts, u"Commanded Actor Ability"},
+ {"CMPX"_ts, u"Complex Scene Object"},
+ {"COEX"_ts, u"Keyword - Conditional Explosion"},
+ {"COOK"_ts, u"Keyword: Cooking Pot"},
+ {"CSTY"_ts, u"Combat Style"},
+ {"CWNE"_ts, u"Keyword - Civil War Neutral"},
+ {"CWOK"_ts, u"Keyword - Civil War Owner"},
+ {"DAED"_ts, u"Keyword - Daedra"},
+ {"DBHF"_ts, u"Dark Brotherhood Faction"},
+ {"DCMS"_ts, u"Dungeon Cleared Music"},
+ {"DCZM"_ts, u"Dragon Crash Zone Marker"},
+ {"DDSC"_ts, u"Dialogue Voice Category"},
+ {"DEIS"_ts, u"Drug Wears Off Image Space"},
+ {"DFMS"_ts, u"Default Music"},
+ {"DFTS"_ts, u"Footstep Set"},
+ {"DGFL"_ts, u"DialogueFollower Quest"},
+ {"DIEN"_ts, u"Keyword - Disallow Enchanting"},
+ {"DLMT"_ts, u"Landscape Material"},
+ {"DLZM"_ts, u"Dragon Land Zone Marker"},
+ {"DMFL"_ts, u"Default MovementType: Fly"},
+ {"DMRN"_ts, u"Default MovementType: Run"},
+ {"DMSN"_ts, u"Default MovementType: Sneak"},
+ {"DMSP"_ts, u"Default MovementType: Sprint"},
+ {"DMSW"_ts, u"Default MovementType: Swim"},
+ {"DMWL"_ts, u"Default MovementType: Walk"},
+ {"DMXL"_ts, u"Dragon Mount No Land List"},
+ {"DOP2"_ts, u"Dialogue Output Model (3D)"},
+ {"DOP3"_ts, u"Dialogue Output Model (2D)"},
+ {"DRAK"_ts, u"Keyword - Dragon"},
+ {"DTMS"_ts, u"Death Music"},
+ {"EACA"_ts, u"Every Actor Ability"},
+ {"EHEQ"_ts, u"EitherHand Equip"},
+ {"EPDF"_ts, u"Eat Package Default Food"},
+ {"FFFP"_ts, u"Keyword - Furniture Forces 1st Person"},
+ {"FFTP"_ts, u"Keyword - Furniture Forces 3rd Person"},
+ {"FGPD"_ts, u"Favor Gifts Per Day"},
+ {"FMFF"_ts, u"Flying Mount - Fly Fast Worldspaces"},
+ {"FMNS"_ts, u"Flying Mount - Disallowed Spells"},
+ {"FMYS"_ts, u"Flying Mount - Allowed Spells"},
+ {"FORG"_ts, u"Keyword: Forge"},
+ {"FPCL"_ts, u"Favor Cost Large"},
+ {"FPCM"_ts, u"Favor Cost Medium"},
+ {"FPCS"_ts, u"Favor Cost Small"},
+ {"FTEL"_ts, u"Male Face Texture Set: Eyes"},
+ {"FTGF"_ts, u"Fighters' Guild Faction"},
+ {"FTHD"_ts, u"Male Face Texture Set: Head"},
+ {"FTHF"_ts, u"Female Face Texture Set: Head"},
+ {"FTMF"_ts, u"Female Face Texture Set: Mouth"},
+ {"FTML"_ts, u"Favor travel marker location"},
+ {"FTMO"_ts, u"Male Face Texture Set: Mouth"},
+ {"FTNP"_ts, u"Furniture Test NPC"},
+ {"FTRF"_ts, u"Female Face Texture Set: Eyes"},
+ {"GCK1"_ts, u"Keyword - Generic Craftable Keyword 01"},
+ {"GCK2"_ts, u"Keyword - Generic Craftable Keyword 02"},
+ {"GCK3"_ts, u"Keyword - Generic Craftable Keyword 03"},
+ {"GCK4"_ts, u"Keyword - Generic Craftable Keyword 04"},
+ {"GCK5"_ts, u"Keyword - Generic Craftable Keyword 05"},
+ {"GCK6"_ts, u"Keyword - Generic Craftable Keyword 06"},
+ {"GCK7"_ts, u"Keyword - Generic Craftable Keyword 07"},
+ {"GCK8"_ts, u"Keyword - Generic Craftable Keyword 08"},
+ {"GCK9"_ts, u"Keyword - Generic Craftable Keyword 09"},
+ {"GCKX"_ts, u"Keyword - Generic Craftable Keyword 10"},
+ {"GFAC"_ts, u"Guard Faction"},
+ {"GOLD"_ts, u"Gold"},
+ {"HBAL"_ts, u"Help - Basic Alchemy"},
+ {"HBAT"_ts, u"Help - Attack Target"},
+ {"HBBR"_ts, u"Help - Barter"},
+ {"HBCO"_ts, u"Help - Basic Cooking"},
+ {"HBEC"_ts, u"Help - Basic Enchanting"},
+ {"HBFG"_ts, u"Help - Basic Forging"},
+ {"HBFM"_ts, u"Help - Flying Mount"},
+ {"HBFS"_ts, u"Help - Favorites"},
+ {"HBFT"_ts, u"Help - Teamate Favor"},
+ {"HBHJ"_ts, u"Help - Jail"},
+ {"HBJL"_ts, u"Help - Journal"},
+ {"HBLH"_ts, u"Help - Low Health"},
+ {"HBLK"_ts, u"Help - Basic Lockpicking (PC)"},
+ {"HBLM"_ts, u"Help - Low Magicka"},
+ {"HBLS"_ts, u"Help - Low Stamina"},
+ {"HBLU"_ts, u"Help - Leveling up"},
+ {"HBLX"_ts, u"Help - Basic Lockpicking (Console)"},
+ {"HBML"_ts, u"Help - Basic Smelting"},
+ {"HBMM"_ts, u"Help - Map Menu"},
+ {"HBOC"_ts, u"Help - Basic Object Creation"},
+ {"HBSA"_ts, u"Help - Basic Smithing Armor"},
+ {"HBSK"_ts, u"Help - Skills Menu"},
+ {"HBSM"_ts, u"Help - Basic Smithing Weapon"},
+ {"HBTA"_ts, u"Help - Basic Tanning"},
+ {"HBTL"_ts, u"Help - Target Lock"},
+ {"HBWC"_ts, u"Help - Weapon Charge"},
+ {"HCLL"_ts, u"FormList - Hair Color List"},
+ {"HFSD"_ts, u"Heartbeat Sound Fast"},
+ {"HMPC"_ts, u"Help Manual PC"},
+ {"HMXB"_ts, u"Help Manual XBox"},
+ {"HRSK"_ts, u"Keyword - Horse"},
+ {"HSSD"_ts, u"Heartbeat Sound Slow"},
+ {"HVFS"_ts, u"Harvest Failed Sound"},
+ {"HVSS"_ts, u"Harvest Sound"},
+ {"IMID"_ts, u"ImageSpaceModifier for inventory menu."},
+ {"IMLH"_ts, u"Imagespace: Low Health"},
+ {"INVP"_ts, u"Inventory Player"},
+ {"IOPM"_ts, u"Interface Output Model"},
+ {"JRLF"_ts, u"Jarl Faction"},
+ {"JWLR"_ts, u"Keyword - Jewelry"},
+ {"KHFL"_ts, u"Kinect Help FormList"},
+ {"KWBR"_ts, u"Keyword - BeastRace"},
+ {"KWCU"_ts, u"Keyword - Cuirass"},
+ {"KWDM"_ts, u"Keyword - DummyObject"},
+ {"KWDO"_ts, u"Keyword - ClearableLocation"},
+ {"KWGE"_ts, u"Keyword - UseGeometryEmitter"},
+ {"KWMS"_ts, u"Keyword - MustStop"},
+ {"KWOT"_ts, u"Keyword - Skip Outfit Items"},
+ {"KWSP"_ts, u"Skyrim - Worldspace"},
+ {"KWUA"_ts, u"Keyword - UpdateDuringArchery"},
+ {"LHEQ"_ts, u"LeftHand Equip"},
+ {"LKHO"_ts, u"Keyword - Hold Location"},
+ {"LKPK"_ts, u"Lockpick"},
+ {"LMHP"_ts, u"Local Map Hide Plane"},
+ {"LRRD"_ts, u"LocRefType - Resource Destructible"},
+ {"LRSO"_ts, u"LocRefType - Civil War Soldier"},
+ {"LRTB"_ts, u"LocRefType Boss"},
+ {"LSIS"_ts, u"Imagespace: Load screen"},
+ {"LUMS"_ts, u"Level Up Music"},
+ {"MDSC"_ts, u"Music Sound Category"},
+ {"MFSN"_ts, u"Magic Fail Sound"},
+ {"MGGF"_ts, u"Mages' Guild Faction"},
+ {"MHFL"_ts, u"Help - Mods"},
+ {"MMCL"_ts, u"Main Menu Cell"},
+ {"MMSD"_ts, u"Map Menu Looping Sound"},
+ {"MNT2"_ts, u"Keyword - Mount"},
+ {"MNTK"_ts, u"Keyword - Mount"},
+ {"MORP"_ts, u""},
+ {"MTSC"_ts, u"Master Sound Category"},
+ {"MVBL"_ts, u"Keyword - Movable"},
+ {"MYSF"_ts, u""},
+ {"MYSN"_ts, u""},
+ {"NASD"_ts, u"No-Activation Sound"},
+ {"NDSC"_ts, u"Non-Dialogue Voice Category"},
+ {"NMRD"_ts, u"Road Marker"},
+ {"NPCK"_ts, u"Keyword - NPC"},
+ {"NRNT"_ts, u"Keyword - Nirnroot"},
+ {"P3OM"_ts, u"Player's Output Model (3rd Person)"},
+ {"PCMD"_ts, u"Player Can Mount Dragon Here List"},
+ {"PDLC"_ts, u"Pause During Loading Menu Category"},
+ {"PDMC"_ts, u"Pause During Menu Category (Fade)"},
+ {"PDSA"_ts, u"Putdown Sound Armor"},
+ {"PDSB"_ts, u"Putdown Sound Book"},
+ {"PDSG"_ts, u"Putdown Sound Generic"},
+ {"PDSI"_ts, u"Putdown Sound Ingredient"},
+ {"PDSW"_ts, u"Putdown Sound Weapon"},
+ {"PFAC"_ts, u"Player Faction"},
+ {"PIMC"_ts, u"Pause During Menu Category (Immediate)"},
+ {"PIVV"_ts, u"Player Is Vampire Variable"},
+ {"PIWV"_ts, u"Player Is Werewolf Variable"},
+ {"PLOC"_ts, u"PersistAll Location"},
+ {"PLST"_ts, u"Default Pack List"},
+ {"POEQ"_ts, u"Potion Equip"},
+ {"POPM"_ts, u"Player's Output Model (1st Person)"},
+ {"PPAR"_ts, u""},
+ {"PTEM"_ts, u"Package template"},
+ {"PTFR"_ts, u"PotentialFollower Faction"},
+ {"PTNP"_ts, u"Pathing Test NPC"},
+ {"PUSA"_ts, u"Pickup Sound Armor"},
+ {"PUSB"_ts, u"Pickup Sound Book"},
+ {"PUSG"_ts, u"Pickup Sound Generic"},
+ {"PUSI"_ts, u"Pickup Sound Ingredient"},
+ {"PUSW"_ts, u"Pickup Sound Weapon"},
+ {"PVFA"_ts, u"Player Voice (Female)"},
+ {"PVFC"_ts, u"Player Voice (Female Child)"},
+ {"PVMA"_ts, u"Player Voice (Male)"},
+ {"PVMC"_ts, u"Player Voice (Male Child)"},
+ {"PWFD"_ts, u"Wait-For-Dialogue Package"},
+ {"RADA"_ts, u""},
+ {"RHEQ"_ts, u"RightHand Equip"},
+ {"RIVR"_ts, u"Vampire Race"},
+ {"RIVS"_ts, u"Vampire Spells"},
+ {"RIWR"_ts, u"Werewolf Race"},
+ {"RUSG"_ts, u"Keyword - Reusable SoulGem"},
+ {"RVBT"_ts, u"Reverb Type"},
+ {"SALT"_ts, u"Sitting Angle Limit"},
+ {"SAT1"_ts, u"Keyword: Scale Actor To 1.0"},
+ {"SCMS"_ts, u"Success Music"},
+ {"SCSD"_ts, u"Soul Captured Sound"},
+ {"SFDC"_ts, u"SFX To Fade In Dialogue Category"},
+ {"SFSN"_ts, u"Shout Fail Sound"},
+ {"SKAB"_ts, u"Survival - Keyword Armor Body"},
+ {"SKAF"_ts, u"Survival - Keyword Armor Feet"},
+ {"SKAH"_ts, u"Survival - Keyword Armor Hands"},
+ {"SKAO"_ts, u"Survival - Keyword Armor Head"},
+ {"SKCB"_ts, u"Survival - Keyword Clothing Body"},
+ {"SKCD"_ts, u"Survival - Keyword Cold"},
+ {"SKCF"_ts, u"Survival - Keyword Clothing Feet"},
+ {"SKCH"_ts, u"Survival - Keyword Clothing Hands"},
+ {"SKCO"_ts, u"Survival - Keyword Clothing Head"},
+ {"SKLK"_ts, u"SkeletonKey"},
+ {"SKWM"_ts, u"Survival - Keyword Warm"},
+ {"SLDM"_ts, u"Snow LOD Material"},
+ {"SLHD"_ts, u"Snow LOD Material (HD)"},
+ {"SMLT"_ts, u"Keyword: Smelter"},
+ {"SMSC"_ts, u"Stats Mute Category"},
+ {"SPFK"_ts, u"Keyword - Special Furniture"},
+ {"SRCP"_ts, u"Survival - Cold Penalty"},
+ {"SRHP"_ts, u"Survival - Hunger Penalty"},
+ {"SRSP"_ts, u"Survival - Sleep Penalty"},
+ {"SRTP"_ts, u"Survival - Temperature"},
+ {"SRVE"_ts, u"Survival Mode Enabled"},
+ {"SRVS"_ts, u"Survival Mode - Show Option"},
+ {"SRVT"_ts, u"Survival Mode - Toggle"},
+ {"SSSC"_ts, u"Stats Music"},
+ {"TANN"_ts, u"Keyword: Tanning Rack"},
+ {"TKAM"_ts, u"Keyword - Type Ammo"},
+ {"TKAR"_ts, u"Keyword - Type Armor"},
+ {"TKBK"_ts, u"Keyword - Type Book"},
+ {"TKGS"_ts, u"Telekinesis Grab Sound"},
+ {"TKIG"_ts, u"Keyword - Type Ingredient"},
+ {"TKKY"_ts, u"Keyword - Type Key"},
+ {"TKMS"_ts, u"Keyword - Type Misc"},
+ {"TKPT"_ts, u"Keyword - Type Potion"},
+ {"TKSG"_ts, u"Keyword - Type SoulGem"},
+ {"TKTS"_ts, u"Telekinesis Throw Sound"},
+ {"TKWP"_ts, u"Keyword - Type Weapon"},
+ {"TSSC"_ts, u"Time Sensitive Sound Category"},
+ {"TVGF"_ts, u"Thieves' Guild Faction"},
+ {"UNDK"_ts, u"Keyword - Undead"},
+ {"URVT"_ts, u"Underwater Reverb Type"},
+ {"UWLS"_ts, u"Underwater Loop Sound"},
+ {"VAMP"_ts, u"Keyword: Vampire"},
+ {"VFNC"_ts, u"Vampire Feed No Crime Faction"},
+ {"VLOC"_ts, u"Virtual Location"},
+ {"VOEQ"_ts, u"Voice Equip"},
+ {"WASN"_ts, u"Ward Absorb Sound"},
+ {"WBSN"_ts, u"Ward Break Sound"},
+ {"WDSN"_ts, u"Ward Deflect Sound"},
+ {"WEML"_ts, u"Weapon Material List"},
+ {"WMDA"_ts, u"Keyword - Weapon Material Daedric"},
+ {"WMDH"_ts, u"Keyword - Weapon Material DraugrHoned"},
+ {"WMDR"_ts, u"Keyword - Weapon Material Draugr"},
+ {"WMDW"_ts, u"Keyword - Weapon Material Dwarven"},
+ {"WMEB"_ts, u"Keyword - Weapon Material Ebony"},
+ {"WMEL"_ts, u"Keyword - Weapon Material Elven"},
+ {"WMFA"_ts, u"Keyword - Weapon Material Falmer"},
+ {"WMFH"_ts, u"Keyword - Weapon Material FalmerHoned"},
+ {"WMGL"_ts, u"Keyword - Weapon Material Glass"},
+ {"WMIM"_ts, u"Keyword - Weapon Material Imperial"},
+ {"WMIR"_ts, u"Keyword - Weapon Material Iron"},
+ {"WMOR"_ts, u"Keyword - Weapon Material Orcish"},
+ {"WMST"_ts, u"Keyword - Weapon Material Steel"},
+ {"WMWE"_ts, u"World Map Weather"},
+ {"WMWO"_ts, u"Keyword - Weapon Material Wood"},
+ {"WPNC"_ts, u"Keyword - Weapon Material Nordic"},
+ {"WPSM"_ts, u"Keyword - Weapon Material Stalhrim"},
+ {"WTBA"_ts, u"Keyword - WeaponTypeBoundArrow"},
+ {"WWSP"_ts, u"Werewolf Spell"},
+ });
+static_assert(std::ranges::is_sorted(DefaultObjectNames));
+
+template <typename K, typename V, std::size_t N>
+inline constexpr V find(const std::array<std::pair<K, V>, N>& table, const K& key)
+{
+ struct GetKey
+ {
+ constexpr K operator()(const std::pair<K, V>& element) noexcept
+ {
+ return element.first;
+ }
+ };
+
+ const auto it = std::ranges::lower_bound(table, key, std::ranges::less{}, GetKey{});
+
+ if (it != std::ranges::end(table) && it->first == key) {
+ return it->second;
+ } else {
+ return V{};
+ }
+}
+
+inline constexpr QStringView getFormName(TESFile::Type type)
+{
+ return find(FormNames, type);
+}
+
+inline constexpr QStringView getDefaultObjectName(TESFile::Type type)
+{
+ return find(DefaultObjectNames, type);
+}
+
+} // namespace TESData
+
+#endif // TESDATA_FORMNAMES_H