From 3949dcfce95af4bd305f258ff5b170d7d50435f6 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sun, 12 Apr 2026 21:04:15 -0500 Subject: 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) --- .../installer_bsplugins/src/TESData/FormParser.cpp | 180 +++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 libs/installer_bsplugins/src/TESData/FormParser.cpp (limited to 'libs/installer_bsplugins/src/TESData/FormParser.cpp') 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 +#include +#include + +namespace TESData +{ + +auto FormParserManager::registrationMap() -> RegistrationMap& +{ + static RegistrationMap registrationMap; + return registrationMap; +} + +std::shared_ptr 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(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(stream); + if (index == 0) { + return u""_s; + } + return u""_s.arg(index); + } else { + std::string str; + std::getline(stream, str, '\0'); + return QString::fromStdString(str); + } +} + +QString readFormId(std::span masters, + const std::string& plugin, std::istream& stream) +{ + const std::uint32_t formId = TESFile::readType(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& 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& 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& indexStack) +{ + item = item->parent(); + indexStack.pop_back(); +} + +[[nodiscard]] static QString readZstring(std::istream& stream) +{ + return QString::fromStdString(TESFile::readZstring(stream)); +} + +template +[[nodiscard]] static QString readWstring(std::istream& stream) +{ + const T length = TESFile::readType(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 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) -- cgit v1.3.1