aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.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/BSPluginInfo/RecordStructureModel.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/BSPluginInfo/RecordStructureModel.cpp')
-rw-r--r--libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp167
1 files changed, 167 insertions, 0 deletions
diff --git a/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp b/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp
new file mode 100644
index 0000000..a28d714
--- /dev/null
+++ b/libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp
@@ -0,0 +1,167 @@
+#include "RecordStructureModel.h"
+#include "TESData/SingleRecordParser.h"
+#include "TESFile/Reader.h"
+
+#include <log.h>
+
+#include <boost/container/flat_map.hpp>
+
+namespace BSPluginInfo
+{
+
+RecordStructureModel::RecordStructureModel(TESData::PluginList* pluginList,
+ TESData::Record* record,
+ const TESData::RecordPath& path,
+ MOBase::IOrganizer* organizer)
+ : m_Organizer{organizer}, m_PluginList{pluginList}, m_Record{record}, m_Path{path},
+ m_Root{std::make_shared<Item>()}
+{
+ refresh();
+}
+
+void RecordStructureModel::refresh()
+{
+ boost::container::flat_map<int, const TESData::FileEntry*> entries;
+ for (const auto handle : m_Record->alternatives()) {
+ const auto entry = m_PluginList->findEntryByHandle(handle);
+ const auto info =
+ m_PluginList->getPluginByName(QString::fromStdString(entry->name()));
+ if (info && info->enabled()) {
+ entries.emplace(info->priority(), entry);
+ }
+ }
+
+ m_Files.resize(entries.size());
+
+ for (int index = static_cast<int>(entries.size()) - 1; index >= 0; --index) {
+ const auto entry = entries.nth(index)->second;
+ const auto name = QString::fromStdString(entry->name());
+ const auto vfsEntry =
+ m_Organizer->virtualFileTree()->find(name, MOBase::FileTreeEntry::FILE);
+
+ const auto filePath = m_Organizer->resolvePath(name);
+ m_Files[index] = QFileInfo(filePath).fileName();
+
+ readFile(m_Path, filePath, index);
+ }
+}
+
+void RecordStructureModel::readFile(const TESData::RecordPath& path,
+ const QString& filePath, int index)
+try {
+ const auto fileName = QFileInfo(filePath).fileName().toStdString();
+ const auto gameName = m_Organizer->managedGame()->gameName();
+ TESData::SingleRecordParser handler(gameName, path, fileName, m_Root.get(), index);
+ TESFile::Reader<TESData::SingleRecordParser> reader{};
+ reader.parse(std::filesystem::path(filePath.toStdWString()), handler);
+} catch (const std::exception& e) {
+ MOBase::log::error("Error parsing \"{}\": {}", filePath, e.what());
+}
+
+QModelIndex RecordStructureModel::index(int row, int column,
+ const QModelIndex& parent) const
+{
+ if (row < 0 || column < 0) {
+ return QModelIndex();
+ }
+
+ const auto parentItem = parent.isValid()
+ ? static_cast<const Item*>(parent.internalPointer())
+ : m_Root.get();
+ if (parentItem && row < parentItem->numChildren()) {
+ const auto childItem = parentItem->childAt(row);
+ return createIndex(row, column, childItem);
+ }
+
+ return QModelIndex();
+}
+
+QModelIndex RecordStructureModel::parent(const QModelIndex& index) const
+{
+ const auto item =
+ index.isValid() ? static_cast<const Item*>(index.internalPointer()) : nullptr;
+ const auto parentItem = item ? item->parent() : nullptr;
+ if (!parentItem) {
+ return QModelIndex();
+ }
+
+ const int row = parentItem->index();
+ return createIndex(row, 0, parentItem);
+}
+
+int RecordStructureModel::rowCount(const QModelIndex& parent) const
+{
+ const auto item = parent.isValid()
+ ? static_cast<const Item*>(parent.internalPointer())
+ : m_Root.get();
+ return item ? item->numChildren() : 0;
+}
+
+int RecordStructureModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+ return 1 + static_cast<int>(m_Files.size());
+}
+
+QVariant RecordStructureModel::data(const QModelIndex& index, int role) const
+{
+ const auto item =
+ index.isValid() ? static_cast<const Item*>(index.internalPointer()) : nullptr;
+ if (!item) {
+ return QVariant();
+ }
+
+ switch (role) {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ if (index.column() == 0) {
+ return item->rowHeader();
+ } else {
+ return item->displayData(index.column() - 1);
+ }
+
+ case Qt::BackgroundRole: {
+ if (index.column() == 0) {
+ return QVariant();
+ }
+
+ const int count = columnCount() - 1;
+ const int fileIndex = index.column() - 1;
+ if (item->isLosingConflict(fileIndex, count)) {
+ return QColor(255, 0, 0, 64);
+ } else if (item->isOverriding(fileIndex)) {
+ return QColor(0, 255, 0, 64);
+ }
+
+ return QVariant();
+ }
+
+ case Qt::UserRole:
+ return QVariant::fromValue(item);
+ }
+
+ return QVariant();
+}
+
+QVariant RecordStructureModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal)
+ return QVariant();
+
+ switch (role) {
+ case Qt::DisplayRole: {
+ if (section == 0) {
+ // Name
+ return QVariant();
+ }
+
+ if (section - 1 < m_Files.length()) {
+ return m_Files[section - 1];
+ }
+ }
+ }
+
+ return QVariant();
+}
+
+} // namespace BSPluginInfo