aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_bsplugins/src/TESData/FileEntry.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-12 21:04:15 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-12 21:04:15 -0500
commit3949dcfce95af4bd305f258ff5b170d7d50435f6 (patch)
tree4c768be256c40f31794c3311f0a2c22933a6705a /libs/installer_bsplugins/src/TESData/FileEntry.cpp
parent892070f3dec1dc690983fd862880e37e711c2516 (diff)
VFS perf fixes, icon/stylesheet compat, download filename fix
VFS: - Open backing fd at mo2_open time for read-only handles so mo2_read can splice without re-opening the file on every read call. - Bump RLIMIT_NOFILE at mount time to fit the resulting fd pressure when the game keeps hundreds of BSAs open. - Dedicated node_cache_mutex so resolveByInode / mo2_lookup stop racing unordered_map writes while multiple tree_mutex readers are active. - max_read=1MB + matching conn->max_read and raised max_readahead/max_write so the kernel merges Wine's small sequential reads into bigger FUSE requests. - Per-op wall-clock counters in mo2_init() logs so we can distinguish VFS latency from game-side work. Proton launch: - Write dxvk.conf to the prefix and set DXVK_CONFIG_FILE to force dxvk.enableGraphicsPipelineLibrary=False, avoiding long GPL compile stalls on first run. UI / plugin compat: - Clamp IconDelegate's per-icon width to [8, 16] so narrow content columns don't trigger QIcon::pixmap(0,0) returning null and logging "failed to load icon" every repaint. - Pre-create lowercase symlinks in the stylesheet directory so QSS files authored on Windows resolve url(foo.svg) when on-disk files are Foo.svg. - Flip content icons empty-chessboard.png and facegen.png to 8-bit RGBA so Qt's built-in PNG reader loads them uniformly. - Add mobase.Version.canonicalString shim for legacy Python plugins that still call the old VersionInfo method on appVersion()'s return. - BSPluginList: compare normalized plugin name lists instead of byte hashes so the post-run case-fix refresh stops spuriously firing the "load order changed" dialog. - BSPluginList disabled by default. Download manager: - Parse CDN URL with QUrl + QUrlQuery and read the filename from the response-content-disposition query param (RFC 6266 quoted / unquoted / ext-value forms), not QFileInfo on the raw URL. - When the API or URL gives us a CDN object key (no archive extension), prefer the actual Content-Disposition header from the live response in metaDataChanged and downloadFinished. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/installer_bsplugins/src/TESData/FileEntry.cpp')
-rw-r--r--libs/installer_bsplugins/src/TESData/FileEntry.cpp227
1 files changed, 227 insertions, 0 deletions
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