diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 21:04:15 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 21:04:15 -0500 |
| commit | 3949dcfce95af4bd305f258ff5b170d7d50435f6 (patch) | |
| tree | 4c768be256c40f31794c3311f0a2c22933a6705a /libs/installer_bsplugins/src/TESData/FormParser.cpp | |
| parent | 892070f3dec1dc690983fd862880e37e711c2516 (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/FormParser.cpp')
| -rw-r--r-- | libs/installer_bsplugins/src/TESData/FormParser.cpp | 180 |
1 files changed, 180 insertions, 0 deletions
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) |
