aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_bsplugins/src/TESData/SingleRecordParser.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/SingleRecordParser.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/SingleRecordParser.cpp')
-rw-r--r--libs/installer_bsplugins/src/TESData/SingleRecordParser.cpp179
1 files changed, 179 insertions, 0 deletions
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