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/TESFile/Stream.h | |
| 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/TESFile/Stream.h')
| -rw-r--r-- | libs/installer_bsplugins/src/TESFile/Stream.h | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/libs/installer_bsplugins/src/TESFile/Stream.h b/libs/installer_bsplugins/src/TESFile/Stream.h new file mode 100644 index 0000000..db22580 --- /dev/null +++ b/libs/installer_bsplugins/src/TESFile/Stream.h @@ -0,0 +1,291 @@ +#ifndef TESFILE_STREAM_H +#define TESFILE_STREAM_H + +#include "Type.h" + +#include <algorithm> +#include <cctype> +#include <cstdint> +#include <cstring> +#include <istream> +#include <ranges> +#include <utility> + +namespace TESFile +{ + +template <typename T> +inline T readType(std::istream& stream) +{ + union + { + char buffer[sizeof(T)]; + T value; + }; + std::memset(buffer, 0x42, sizeof(T)); + stream.read(buffer, sizeof(T)); + return value; +} + +inline std::string readZstring(std::istream& stream) +{ + std::string value; + std::getline(stream, value, '\0'); + return value; +} + +inline int icompare(const std::string& a, const std::string& b) +{ + const std::size_t n = std::min(a.size(), b.size()); + for (std::size_t i = 0; i < n; ++i) { + const unsigned char ca = + static_cast<unsigned char>(std::tolower(static_cast<unsigned char>(a[i]))); + const unsigned char cb = + static_cast<unsigned char>(std::tolower(static_cast<unsigned char>(b[i]))); + if (ca != cb) + return ca < cb ? -1 : 1; + } + if (a.size() == b.size()) + return 0; + return a.size() < b.size() ? -1 : 1; +} + +inline bool iequals(const std::string& a, const std::string& b) +{ + return icompare(a, b) == 0; +} + +template <std::ranges::input_range R, typename Proj = std::identity> +inline auto find(R&& r, const std::string& value, Proj proj = {}) +{ + return std::ranges::find_if( + r, + [&](auto&& val) { + return iequals(val, value); + }, + proj); +} + +struct less +{ + bool operator()(const std::string& a, const std::string& b) const + { + return icompare(a, b) < 0; + } +}; + +enum class TESFormat +{ + Standard, + Oblivion, + Morrowind, +}; + +enum class GroupType : std::int32_t +{ + Top, + WorldChildren, + InteriorCellBlock, + InteriorCellSubBlock, + ExteriorCellBlock, + ExteriorCellSubBlock, + CellChildren, + TopicChildren, + CellPersistentChildren, + CellTemporaryChildren, + QuestChildren, + CellVisibleDistantChildren = 10, +}; + +struct RecordFlags +{ + enum TES4Flag : std::uint32_t + { + Master = 0x1, + Optimized = 0x10, + Localized = 0x80, + SmallNew = 0x100, + SmallOld = 0x200, + Update = 0x200, + Medium = 0x400, + Blueprint = 0x800, + }; + + enum Flag : std::uint32_t + { + Compressed = 0x40000, + }; +}; + +struct RecordHeader +{ + Type type; + std::uint32_t dataSize; + union + { + struct + { + std::uint32_t flags; + std::uint32_t formId; + } formData; + struct + { + std::uint32_t label; + GroupType groupType; + } groupData; + }; + union + { + struct + { + std::uint32_t revision; + Type firstChunk; + } old; + struct + { + std::uint16_t timestamp; + std::uint16_t revision; + std::uint16_t version; + std::uint16_t unknown1; + }; + }; +}; +static_assert(sizeof(RecordHeader) == 24); + +struct ChunkHeader +{ + Type type; + std::uint16_t dataSize; +}; +static_assert(sizeof(ChunkHeader) == 6); + +class GroupData final +{ +public: + constexpr GroupData() : formId_{0}, groupType_{GroupType::Top} {} + + constexpr GroupData(std::uint32_t label, GroupType type) + : formId_{label}, groupType_{type} + {} + + constexpr auto operator<=>(const GroupData& other) const + { + if (groupType_ != other.groupType_) { + return groupType_ <=> other.groupType_; + } else { + if (hasFormType()) { + return formType() <=> other.formType(); + } else if (hasBlock()) { + return block() <=> other.block(); + } else if (hasGridCell()) { + return gridCell() <=> other.gridCell(); + } else { + return formId_ <=> other.formId_; + } + } + } + + constexpr bool operator==(const GroupData& other) const + { + return groupType_ == other.groupType_ && formId_ == other.formId_; + } + + [[nodiscard]] constexpr GroupType type() const { return groupType_; } + + [[nodiscard]] constexpr bool hasFormType() const + { + return groupType_ == GroupType::Top; + } + + [[nodiscard]] constexpr bool hasParent() const + { + return groupType_ == GroupType::WorldChildren || + groupType_ == GroupType::CellChildren || + groupType_ == GroupType::TopicChildren || + groupType_ == GroupType::CellPersistentChildren || + groupType_ == GroupType::CellTemporaryChildren || + groupType_ == GroupType::QuestChildren; + } + + [[nodiscard]] constexpr bool hasDirectParent() const + { + return groupType_ == GroupType::WorldChildren || + groupType_ == GroupType::CellChildren || + groupType_ == GroupType::TopicChildren || + groupType_ == GroupType::QuestChildren; + } + + [[nodiscard]] constexpr bool hasBlock() const + { + return groupType_ == GroupType::InteriorCellBlock || + groupType_ == GroupType::InteriorCellSubBlock; + } + + [[nodiscard]] constexpr bool hasGridCell() const + { + return groupType_ == GroupType::ExteriorCellBlock || + groupType_ == GroupType::ExteriorCellSubBlock; + } + + [[nodiscard]] constexpr Type formType() const { return formType_; } + + [[nodiscard]] constexpr std::uint32_t parent() const { return formId_; } + + [[nodiscard]] constexpr std::int32_t block() const { return number_; } + + [[nodiscard]] constexpr std::pair<std::int16_t, std::int16_t> gridCell() const + { + return {cell_.x, cell_.y}; + } + + constexpr void setLocalIndex(std::uint8_t index) + { + if (hasParent()) { + formId_ = (formId_ & 0xFFFFFFU) | (index << 24U); + } + } + +private: + GroupType groupType_; + union + { + Type formType_; + std::uint32_t formId_; + std::int32_t number_; + struct + { + std::int16_t y; + std::int16_t x; + } cell_; + }; +}; + +class FormData final +{ +public: + constexpr FormData(Type type, std::uint32_t flags, std::uint32_t formId, std::uint16_t formVersion) + : type_{type}, flags_{flags}, formId_{formId}, formVersion_{formVersion} + {} + + [[nodiscard]] constexpr Type type() const { return type_; } + + [[nodiscard]] constexpr std::uint32_t flags() const { return flags_; } + + [[nodiscard]] constexpr std::uint32_t formId() const { return formId_; } + + [[nodiscard]] constexpr std::uint16_t formVersion() const { return formVersion_; } + + [[nodiscard]] constexpr std::uint8_t localModIndex() const { return formId_ >> 24; } + +private: + Type type_; + std::uint32_t flags_; + std::uint32_t formId_; + std::uint16_t formVersion_; +}; + +} // namespace TESFile + +using namespace TESFile::literals; + +#endif // TESFILE_STREAM_H |
