diff options
Diffstat (limited to 'libs/installer_bsplugins/src/TESData')
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 |
