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/BSPluginInfo/RecordStructureModel.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/BSPluginInfo/RecordStructureModel.cpp')
| -rw-r--r-- | libs/installer_bsplugins/src/BSPluginInfo/RecordStructureModel.cpp | 167 |
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 |
