aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-12 17:55:56 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-12 17:55:56 -0500
commit892070f3dec1dc690983fd862880e37e711c2516 (patch)
tree423da9fca609a9239efbb6c74ab2e8657975c5a4 /libs
parenta0355c9b897281d0bfea48bec9d490724ecabd96 (diff)
Add BSA in-memory extraction and nested preview support
bsatk: - Archive::extractToMemory() extracts a file directly into a byte buffer instead of a filesystem path, backed by a std::ostringstream sink. - Archive::findFile() locates a file by relative path, case-insensitive and separator-agnostic, walking the folder tree recursively. - extractDirect/extractCompressed now take std::ostream& so both the ofstream and ostringstream paths share the same implementation. libbsarch: shim rewrite on Linux to map the Windows API surface onto bsatk::Archive, enabling preview_bsa to open vanilla BSAs via the same interface as other plugins. preview_bsa: double-click on a file in the BSA tree now extracts the bytes via extractToMemory() and hands them to IOrganizer::previewFileData for nested preview rendering (e.g. NIF inside a BSA). uibase + organizerproxy + organizercore: new IOrganizer::previewFileData virtual that routes in-memory file bytes through the first registered IPluginPreview matching the extension. Uses heap + WA_DeleteOnClose + open() to avoid nested exec() softlock from mod info filetree -> preview_bsa tree -> nif preview. docker/build-inner.sh: stage preview_nif shaders into plugin_data/shaders so ShaderManager can load them via IOrganizer::getPluginDataPath(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs')
-rw-r--r--libs/bsatk/include/bsatk/bsaarchive.h21
-rw-r--r--libs/bsatk/src/bsaarchive.cpp102
-rw-r--r--libs/libbsarch/CMakeLists.txt16
-rw-r--r--libs/libbsarch/src/libbsarch.cpp460
-rw-r--r--libs/preview_bsa/src/previewbsa.cpp79
-rw-r--r--libs/preview_bsa/src/previewbsa.h2
-rw-r--r--libs/preview_bsa/src/simplefiletreeitem.cpp12
-rw-r--r--libs/preview_bsa/src/simplefiletreeitem.h7
-rw-r--r--libs/uibase/include/uibase/imoinfo.h21
9 files changed, 598 insertions, 122 deletions
diff --git a/libs/bsatk/include/bsatk/bsaarchive.h b/libs/bsatk/include/bsatk/bsaarchive.h
index cd15f64..915b9fb 100644
--- a/libs/bsatk/include/bsatk/bsaarchive.h
+++ b/libs/bsatk/include/bsatk/bsaarchive.h
@@ -112,6 +112,23 @@ public:
* @return ERROR_NONE on success or an error code
*/
EErrorCode extract(File::Ptr file, const char* outputDirectory) const;
+
+ /**
+ * extract a file from the archive directly into an in-memory buffer.
+ * @param file descriptor of the file to extract
+ * @param result output buffer; cleared and filled with uncompressed file data
+ * @return ERROR_NONE on success or an error code
+ */
+ EErrorCode extractToMemory(File::Ptr file,
+ std::vector<unsigned char>& result) const;
+
+ /**
+ * find a file in the archive by its relative path.
+ * @param path relative path inside the archive (separators may be / or \,
+ * matching is case-insensitive).
+ * @return the file descriptor, or nullptr if no such file exists.
+ */
+ File::Ptr findFile(const std::string& path) const;
/**
* @return archive flags
*/
@@ -211,8 +228,8 @@ private:
void getDX10Header(DirectX::DDS_HEADER_DXT10& DX10Header, File::Ptr file,
DirectX::DDS_HEADER DDSHeader) const;
- EErrorCode extractDirect(File::Ptr file, std::ofstream& outFile) const;
- EErrorCode extractCompressed(File::Ptr file, std::ofstream& outFile) const;
+ EErrorCode extractDirect(File::Ptr file, std::ostream& outFile) const;
+ EErrorCode extractCompressed(File::Ptr file, std::ostream& outFile) const;
void createFolders(const std::string& targetDirectory, Folder::Ptr folder);
diff --git a/libs/bsatk/src/bsaarchive.cpp b/libs/bsatk/src/bsaarchive.cpp
index 172a0b2..0ec9e20 100644
--- a/libs/bsatk/src/bsaarchive.cpp
+++ b/libs/bsatk/src/bsaarchive.cpp
@@ -27,6 +27,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <boost/shared_array.hpp>
#include <boost/thread.hpp>
+#include <cctype>
#include <cstring>
#include <fstream>
#include <iostream>
@@ -34,6 +35,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <lz4frame.h>
#include <memory>
#include <queue>
+#include <sstream>
#include <sys/stat.h>
#include <zlib.h>
#ifdef _WIN32
@@ -621,7 +623,7 @@ void Archive::getDX10Header(DirectX::DDS_HEADER_DXT10& DX10Header, File::Ptr fil
static const BSAULong CHUNK_SIZE = 128 * 1024;
-EErrorCode Archive::extractDirect(File::Ptr file, std::ofstream& outFile) const
+EErrorCode Archive::extractDirect(File::Ptr file, std::ostream& outFile) const
{
EErrorCode result = ERROR_NONE;
if (file->m_FileSize == 0) {
@@ -744,7 +746,7 @@ std::shared_ptr<unsigned char[]> Archive::decompress(unsigned char* inBuffer,
}
}
-EErrorCode Archive::extractCompressed(File::Ptr file, std::ofstream& outFile) const
+EErrorCode Archive::extractCompressed(File::Ptr file, std::ostream& outFile) const
{
EErrorCode result = ERROR_NONE;
if (file->m_FileSize == 0) {
@@ -881,6 +883,102 @@ EErrorCode Archive::extract(File::Ptr file, const char* outputDirectory) const
return result;
}
+EErrorCode Archive::extractToMemory(File::Ptr file,
+ std::vector<unsigned char>& result) const
+{
+ result.clear();
+ if (!file) {
+ return ERROR_INVALIDDATA;
+ }
+
+ std::ostringstream stream(std::ios::out | std::ios::binary);
+ EErrorCode rc = ERROR_NONE;
+ if (compressed(file)) {
+ rc = extractCompressed(file, stream);
+ } else {
+ rc = extractDirect(file, stream);
+ }
+
+ if (rc != ERROR_NONE) {
+ return rc;
+ }
+
+ const std::string& data = stream.str();
+ result.assign(reinterpret_cast<const unsigned char*>(data.data()),
+ reinterpret_cast<const unsigned char*>(data.data() + data.size()));
+ return ERROR_NONE;
+}
+
+static bool iequals_bsa(const std::string& lhs, const std::string& rhs)
+{
+ if (lhs.size() != rhs.size()) {
+ return false;
+ }
+ for (std::size_t i = 0; i < lhs.size(); ++i) {
+ if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
+ std::tolower(static_cast<unsigned char>(rhs[i]))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Normalize a path for case-insensitive, separator-agnostic comparison:
+// all forward slashes, all lowercase. Used by findFile only — doesn't mutate
+// stored names.
+static std::string normalize_bsa_path(const std::string& in)
+{
+ std::string out;
+ out.reserve(in.size());
+ for (char ch : in) {
+ if (ch == '\\') {
+ ch = '/';
+ }
+ out.push_back(static_cast<char>(
+ std::tolower(static_cast<unsigned char>(ch))));
+ }
+ // strip any leading separator
+ std::size_t start = 0;
+ while (start < out.size() && out[start] == '/') {
+ ++start;
+ }
+ return out.substr(start);
+}
+
+static void collect_files_recursive(Folder::Ptr folder,
+ std::vector<File::Ptr>& out)
+{
+ for (unsigned int i = 0; i < folder->getNumFiles(); ++i) {
+ out.push_back(folder->getFile(i));
+ }
+ for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) {
+ collect_files_recursive(folder->getSubFolder(i), out);
+ }
+}
+
+File::Ptr Archive::findFile(const std::string& path) const
+{
+ if (!m_RootFolder || path.empty()) {
+ return nullptr;
+ }
+
+ const std::string want = normalize_bsa_path(path);
+
+ // BSAs store flat folder names like "meshes\\actors\\character". On Linux
+ // std::filesystem::path doesn't treat '\\' as a separator, so bsatk builds
+ // a flat folder tree there. Rather than trying to reverse-engineer the
+ // structure, iterate every File in the archive and compare its full path
+ // (normalized) against the caller-supplied one.
+ std::vector<File::Ptr> allFiles;
+ collect_files_recursive(m_RootFolder, allFiles);
+ for (const File::Ptr& entry : allFiles) {
+ if (normalize_bsa_path(entry->getFilePath()) == want) {
+ return entry;
+ }
+ }
+ return nullptr;
+}
+
void Archive::readFiles(std::queue<FileInfo>& queue, boost::mutex& mutex,
boost::interprocess::interprocess_semaphore& bufferCount,
boost::interprocess::interprocess_semaphore& queueFree,
diff --git a/libs/libbsarch/CMakeLists.txt b/libs/libbsarch/CMakeLists.txt
index d3e994e..db92b91 100644
--- a/libs/libbsarch/CMakeLists.txt
+++ b/libs/libbsarch/CMakeLists.txt
@@ -29,12 +29,24 @@ else()
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include
FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.h
)
- # Set default symbol visibility to hidden, export only marked symbols
+ # Set default symbol visibility to hidden, export only marked symbols.
+ # libbsarch.cpp uses BSARCH_DLL_API which maps to
+ # __attribute__((visibility("default"))) when BSARCH_DLL_EXPORT is set,
+ # so the C API stays exported while the bsatk bridge stays internal.
target_compile_options(libbsarch PRIVATE -fvisibility=hidden)
endif()
target_include_directories(libbsarch PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch)
-set_target_properties(libbsarch PROPERTIES DEBUG_POSTFIX "d" CXX_STANDARD 11)
+if(WIN32)
+ set_target_properties(libbsarch PROPERTIES DEBUG_POSTFIX "d" CXX_STANDARD 11)
+else()
+ # Linux impl is backed by bsatk and needs C++17 for std::string::data()
+ # non-const and the rest of the bsatk API.
+ set_target_properties(libbsarch PROPERTIES DEBUG_POSTFIX "d" CXX_STANDARD 17)
+endif()
target_link_libraries(libbsarch PUBLIC mo2::dds-header)
+if(NOT WIN32)
+ target_link_libraries(libbsarch PRIVATE mo2::bsatk)
+endif()
target_compile_definitions(libbsarch PRIVATE BSARCH_DLL_EXPORT)
# preprocessor definitions
diff --git a/libs/libbsarch/src/libbsarch.cpp b/libs/libbsarch/src/libbsarch.cpp
index 52ded9f..1f98ddc 100644
--- a/libs/libbsarch/src/libbsarch.cpp
+++ b/libs/libbsarch/src/libbsarch.cpp
@@ -1,206 +1,464 @@
#ifdef BSARCH_DLL_EXPORT
#include "libbsarch.h"
+#include <algorithm>
+#include <cstring>
+#include <cwchar>
+#include <string>
+
+#ifndef _WIN32
+#include <bsatk/bsatk.h>
+
+#include <memory>
+#include <unordered_map>
+#include <vector>
+#endif
+
namespace libbsarch {
-BSARCH_DLL_API(bsa_entry_list_t) bsa_entry_list_create()
-{
- return nullptr;
-}
+#ifndef _WIN32
-BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_free(bsa_entry_list_t entry_list)
-{
- return {0};
-}
+// ---------------------------------------------------------------------------
+// Linux implementation backed by bsatk.
+//
+// libbsarch is a Delphi-based archive library on Windows. Upstream MO2 uses
+// a prebuilt DLL from vcpkg. On Linux we have no such runtime, so previously
+// this file was a set of no-op stubs — which meant anything that went through
+// the C++ wrappers (bs_archive::load_from_disk / extract_to_memory, used e.g.
+// by the data-tab preview pipeline in OrganizerCore) silently returned empty.
+//
+// This replacement wires the read path through BSA::Archive from libs/bsatk,
+// which is a pure C++ BSA reader already built and linked in MO2. The write
+// side (creating archives, adding files, saving) is still stubbed; bsapacker
+// linked against that stub before and keeps linking against it now.
+// ---------------------------------------------------------------------------
-BSARCH_DLL_API(uint32_t) bsa_entry_list_count(bsa_entry_list_t entry_list)
+struct LinuxArchive
{
- return 0;
-}
+ BSA::Archive archive;
+ std::string filename;
+ bool loaded = false;
+
+ // Keep shared_ptrs alive so we can hand out a raw File* as an opaque
+ // record id and safely dereference it later from *_by_record calls.
+ std::unordered_map<BSA::File*, BSA::File::Ptr> fileRecords;
+};
-BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_add(bsa_entry_list_t entry_list, const wchar_t *entry_string)
+static LinuxArchive* as_archive(bsa_archive_t handle)
{
- return {0};
+ return static_cast<LinuxArchive*>(handle);
}
-BSARCH_DLL_API(uint32_t)
-bsa_entry_list_get(bsa_entry_list_t entry_list, uint32_t index, uint32_t string_buffer_size, wchar_t *string_buffer)
+static std::string wchar_to_utf8(const wchar_t* ws)
{
- return 0;
+ if (!ws) return {};
+ std::mbstate_t state{};
+ const wchar_t* src = ws;
+ std::size_t len = std::wcsrtombs(nullptr, &src, 0, &state);
+ if (len == static_cast<std::size_t>(-1)) {
+ // Fallback: BSA paths are ASCII, just truncate each wchar to its low
+ // byte. Good enough for any real Bethesda archive path.
+ std::string out;
+ for (const wchar_t* p = ws; *p; ++p) {
+ out.push_back(static_cast<char>(*p & 0x7F));
+ }
+ return out;
+ }
+ std::string out(len, '\0');
+ state = {};
+ src = ws;
+ std::wcsrtombs(out.data(), &src, len + 1, &state);
+ return out;
}
-BSARCH_DLL_API(bsa_archive_t) bsa_create()
+static void copy_to_wbuffer(const std::string& src, uint32_t buf_size, wchar_t* buf)
{
- return nullptr;
+ if (!buf || buf_size == 0) return;
+ std::mbstate_t state{};
+ const char* p = src.c_str();
+ std::size_t len = std::mbsrtowcs(buf, &p, buf_size - 1, &state);
+ if (len == static_cast<std::size_t>(-1)) {
+ // ASCII fallback
+ std::size_t i = 0;
+ for (; i + 1 < buf_size && i < src.size(); ++i) {
+ buf[i] = static_cast<wchar_t>(static_cast<unsigned char>(src[i]));
+ }
+ buf[i] = 0;
+ return;
+ }
+ const std::size_t last = std::min<std::size_t>(len, buf_size - 1);
+ buf[last] = 0;
}
-BSARCH_DLL_API(bsa_result_message_t) bsa_free(bsa_archive_t archive)
+static bsa_result_message_t make_message(bsa_result_code_t code)
{
- return {0};
+ bsa_result_message_t r{};
+ r.code = static_cast<int8_t>(code);
+ return r;
}
-BSARCH_DLL_API(bsa_result_message_t) bsa_load_from_file(bsa_archive_t archive, const wchar_t *file_path)
+static bsa_result_message_buffer_t make_extract_result(
+ const std::vector<unsigned char>& data)
{
- return {0};
+ bsa_result_message_buffer_t out{};
+ out.message.code = BSA_RESULT_NONE;
+ out.buffer.size = static_cast<uint32_t>(data.size());
+ if (!data.empty()) {
+ auto* copy = new unsigned char[data.size()];
+ std::memcpy(copy, data.data(), data.size());
+ out.buffer.data = copy;
+ } else {
+ out.buffer.data = nullptr;
+ }
+ return out;
}
-BSARCH_DLL_API(bsa_result_message_t)
-bsa_create_archive(bsa_archive_t archive,
- const wchar_t *file_path,
- bsa_archive_type_t archive_type,
- bsa_entry_list_t entry_list)
+static bsa_result_message_buffer_t make_extract_error()
{
- return {0};
+ bsa_result_message_buffer_t out{};
+ out.message.code = BSA_RESULT_EXCEPTION;
+ out.buffer.size = 0;
+ out.buffer.data = nullptr;
+ return out;
}
-BSARCH_DLL_API(bsa_result_message_t) bsa_save(bsa_archive_t archive)
+// --- Lifecycle ---
+
+BSARCH_DLL_API(bsa_archive_t) bsa_create()
{
- return {0};
+ return static_cast<bsa_archive_t>(new LinuxArchive());
}
-BSARCH_DLL_API(bsa_result_message_t)
-bsa_add_file_from_disk(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *source_path)
+BSARCH_DLL_API(bsa_result_message_t) bsa_free(bsa_archive_t handle)
{
- return {0};
+ delete as_archive(handle);
+ return make_message(BSA_RESULT_NONE);
}
-BSARCH_DLL_API(bsa_result_message_t)
-bsa_add_file_from_disk_root(bsa_archive_t archive, const wchar_t *root_dir, const wchar_t *source_path)
+BSARCH_DLL_API(bsa_result_message_t) bsa_close(bsa_archive_t handle)
{
- return {0};
+ if (auto* wrapper = as_archive(handle)) {
+ wrapper->archive.close();
+ wrapper->fileRecords.clear();
+ wrapper->loaded = false;
+ }
+ return make_message(BSA_RESULT_NONE);
}
+// --- Reading ---
+
BSARCH_DLL_API(bsa_result_message_t)
-bsa_add_file_from_memory(bsa_archive_t archive, const wchar_t *file_path, uint32_t size, bsa_buffer_t data)
+bsa_load_from_file(bsa_archive_t handle, const wchar_t* file_path)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !file_path) {
+ return make_message(BSA_RESULT_EXCEPTION);
+ }
+ wrapper->filename = wchar_to_utf8(file_path);
+ const BSA::EErrorCode rc =
+ wrapper->archive.read(wrapper->filename.c_str(), false);
+ if (rc != BSA::ERROR_NONE && rc != BSA::ERROR_INVALIDHASHES) {
+ return make_message(BSA_RESULT_EXCEPTION);
+ }
+ wrapper->loaded = true;
+ return make_message(BSA_RESULT_NONE);
}
-BSARCH_DLL_API(bsa_file_record_t) bsa_find_file_record(bsa_archive_t archive, const wchar_t *file_path)
+BSARCH_DLL_API(bsa_file_record_t)
+bsa_find_file_record(bsa_archive_t handle, const wchar_t* file_path)
{
- return nullptr;
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded || !file_path) return nullptr;
+
+ BSA::File::Ptr file = wrapper->archive.findFile(wchar_to_utf8(file_path));
+ if (!file) return nullptr;
+
+ BSA::File* raw = file.get();
+ wrapper->fileRecords[raw] = file;
+ return static_cast<bsa_file_record_t>(raw);
}
BSARCH_DLL_API(bsa_result_message_buffer_t)
-bsa_extract_file_data_by_record(bsa_archive_t archive, bsa_file_record_t file_record)
+bsa_extract_file_data_by_filename(bsa_archive_t handle, const wchar_t* file_path)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded || !file_path) {
+ return make_extract_error();
+ }
+ BSA::File::Ptr file = wrapper->archive.findFile(wchar_to_utf8(file_path));
+ if (!file) {
+ return make_extract_error();
+ }
+ std::vector<unsigned char> data;
+ if (wrapper->archive.extractToMemory(file, data) != BSA::ERROR_NONE) {
+ return make_extract_error();
+ }
+ return make_extract_result(data);
}
BSARCH_DLL_API(bsa_result_message_buffer_t)
-bsa_extract_file_data_by_filename(bsa_archive_t archive, const wchar_t *file_path)
+bsa_extract_file_data_by_record(bsa_archive_t handle, bsa_file_record_t record)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded || !record) {
+ return make_extract_error();
+ }
+ auto it = wrapper->fileRecords.find(static_cast<BSA::File*>(record));
+ if (it == wrapper->fileRecords.end()) {
+ return make_extract_error();
+ }
+ std::vector<unsigned char> data;
+ if (wrapper->archive.extractToMemory(it->second, data) != BSA::ERROR_NONE) {
+ return make_extract_error();
+ }
+ return make_extract_result(data);
}
-BSARCH_DLL_API(bsa_result_message_t) bsa_file_data_free(bsa_archive_t archive, bsa_result_buffer_t file_data_result)
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_file_data_free(bsa_archive_t, bsa_result_buffer_t buffer)
{
- return {0};
+ delete[] static_cast<unsigned char*>(buffer.data);
+ return make_message(BSA_RESULT_NONE);
}
-BSARCH_DLL_API(bsa_result_message_t)
-bsa_extract_file(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *save_as)
+BSARCH_DLL_API(bool)
+bsa_file_exists(bsa_archive_t handle, const wchar_t* file_path)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded || !file_path) return false;
+ return wrapper->archive.findFile(wchar_to_utf8(file_path)) != nullptr;
}
-BSARCH_DLL_API(bsa_result_message_t)
-bsa_iterate_files(bsa_archive_t archive, bsa_file_iteration_proc_t file_iteration_proc, void *context)
+// --- Metadata ---
+
+BSARCH_DLL_API(uint32_t)
+bsa_filename_get(bsa_archive_t handle, uint32_t buf_size, wchar_t* buf)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper) return 0;
+ copy_to_wbuffer(wrapper->filename, buf_size, buf);
+ return static_cast<uint32_t>(wrapper->filename.size());
}
-BSARCH_DLL_API(bool) bsa_file_exists(bsa_archive_t archive, const wchar_t *file_path)
+BSARCH_DLL_API(bsa_archive_type_t)
+bsa_archive_type_get(bsa_archive_t handle)
{
- return false;
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded) return baNone;
+ switch (wrapper->archive.getType()) {
+ case TYPE_MORROWIND:
+ return baTES3;
+ case TYPE_OBLIVION:
+ return baTES4;
+ case TYPE_FALLOUT3:
+ return baFO3;
+ case TYPE_SKYRIMSE:
+ return baSSE;
+ case TYPE_FALLOUT4:
+ case TYPE_FALLOUT4NG_7:
+ case TYPE_FALLOUT4NG_8:
+ return baFO4;
+ case TYPE_STARFIELD:
+ return baSF;
+ case TYPE_STARFIELD_LZ4_TEXTURE:
+ return baSFdds;
+ default:
+ return baNone;
+ }
}
-BSARCH_DLL_API(bsa_result_message_t)
-bsa_get_resource_list(bsa_archive_t archive, bsa_entry_list_t entry_result_list, const wchar_t *folder)
+BSARCH_DLL_API(uint32_t) bsa_version_get(bsa_archive_t handle)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded) return 0;
+ return static_cast<uint32_t>(wrapper->archive.getType());
}
-BSARCH_DLL_API(bsa_result_message_t)
-bsa_resolve_hash(bsa_archive_t archive, uint64_t hash, bsa_entry_list_t entry_result_list)
+BSARCH_DLL_API(uint32_t)
+bsa_format_name_get(bsa_archive_t handle, uint32_t buf_size, wchar_t* buf)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper) return 0;
+ const char* name = "BSA";
+ if (wrapper->loaded) {
+ switch (wrapper->archive.getType()) {
+ case TYPE_MORROWIND:
+ name = "Morrowind";
+ break;
+ case TYPE_OBLIVION:
+ name = "Oblivion";
+ break;
+ case TYPE_FALLOUT3:
+ name = "Fallout 3";
+ break;
+ case TYPE_SKYRIMSE:
+ name = "Skyrim Special Edition";
+ break;
+ case TYPE_FALLOUT4:
+ case TYPE_FALLOUT4NG_7:
+ case TYPE_FALLOUT4NG_8:
+ name = "Fallout 4";
+ break;
+ case TYPE_STARFIELD:
+ name = "Starfield";
+ break;
+ case TYPE_STARFIELD_LZ4_TEXTURE:
+ name = "Starfield DDS";
+ break;
+ default:
+ break;
+ }
+ }
+ const std::string s(name);
+ copy_to_wbuffer(s, buf_size, buf);
+ return static_cast<uint32_t>(s.size());
}
-BSARCH_DLL_API(bsa_result_message_t) bsa_close(bsa_archive_t archive)
+BSARCH_DLL_API(uint32_t) bsa_file_count_get(bsa_archive_t handle)
{
- return {0};
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded) return 0;
+ BSA::Folder::Ptr root = wrapper->archive.getRoot();
+ return root ? root->countFiles() : 0;
}
-BSARCH_DLL_API(uint32_t) bsa_filename_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer)
+BSARCH_DLL_API(uint32_t) bsa_archive_flags_get(bsa_archive_t handle)
{
- return 0;
+ auto* wrapper = as_archive(handle);
+ if (!wrapper || !wrapper->loaded) return 0;
+ return wrapper->archive.getFlags();
}
-BSARCH_DLL_API(bsa_archive_type_t) bsa_archive_type_get(bsa_archive_t archive)
+// --- Write-side + misc no-ops (bsapacker links these; writes are not
+// implemented on Linux yet, same as before this refactor). ---
+
+BSARCH_DLL_API(bsa_entry_list_t) bsa_entry_list_create()
{
- return bsa_archive_type_t::baNone;
+ return nullptr;
}
-
-BSARCH_DLL_API(uint32_t) bsa_version_get(bsa_archive_t archive)
+BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_free(bsa_entry_list_t)
{
- return 0;
+ return make_message(BSA_RESULT_NONE);
}
-
-BSARCH_DLL_API(uint32_t) bsa_format_name_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer)
+BSARCH_DLL_API(uint32_t) bsa_entry_list_count(bsa_entry_list_t)
{
return 0;
}
-
-BSARCH_DLL_API(uint32_t) bsa_file_count_get(bsa_archive_t archive)
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_entry_list_add(bsa_entry_list_t, const wchar_t*)
{
- return 0;
+ return make_message(BSA_RESULT_NONE);
}
-
-BSARCH_DLL_API(uint32_t) bsa_archive_flags_get(bsa_archive_t archive)
+BSARCH_DLL_API(uint32_t)
+bsa_entry_list_get(bsa_entry_list_t, uint32_t, uint32_t, wchar_t*)
{
return 0;
}
-
-BSARCH_DLL_API(void) bsa_archive_flags_set(bsa_archive_t archive, uint32_t flags)
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_create_archive(bsa_archive_t, const wchar_t*, bsa_archive_type_t,
+ bsa_entry_list_t)
{
- return;
+ return make_message(BSA_RESULT_NONE);
}
-
-BSARCH_DLL_API(uint32_t) bsa_file_flags_get(bsa_archive_t archive)
+BSARCH_DLL_API(bsa_result_message_t) bsa_save(bsa_archive_t)
{
- return 0;
+ return make_message(BSA_RESULT_NONE);
}
-
-BSARCH_DLL_API(void) bsa_file_flags_set(bsa_archive_t archive, uint32_t flags)
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_add_file_from_disk(bsa_archive_t, const wchar_t*, const wchar_t*)
{
- return;
+ return make_message(BSA_RESULT_NONE);
}
-
-BSARCH_DLL_API(bool) bsa_compress_get(bsa_archive_t archive)
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_add_file_from_disk_root(bsa_archive_t, const wchar_t*, const wchar_t*)
{
- return false;
+ return make_message(BSA_RESULT_NONE);
}
-
-BSARCH_DLL_API(void) bsa_compress_set(bsa_archive_t archive, bool flags)
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_add_file_from_memory(bsa_archive_t, const wchar_t*, uint32_t, bsa_buffer_t)
{
- return;
+ return make_message(BSA_RESULT_NONE);
}
-
-BSARCH_DLL_API(bool) bsa_share_data_get(bsa_archive_t archive)
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_extract_file(bsa_archive_t, const wchar_t*, const wchar_t*)
+{
+ return make_message(BSA_RESULT_NONE);
+}
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_iterate_files(bsa_archive_t, bsa_file_iteration_proc_t, void*)
+{
+ return make_message(BSA_RESULT_NONE);
+}
+BSARCH_DLL_API(bsa_result_message_t)
+bsa_get_resource_list(bsa_archive_t, bsa_entry_list_t, const wchar_t*)
+{
+ return make_message(BSA_RESULT_NONE);
+}
+BSARCH_DLL_API(void) bsa_archive_flags_set(bsa_archive_t, uint32_t) {}
+BSARCH_DLL_API(uint32_t) bsa_file_flags_get(bsa_archive_t)
+{
+ return 0;
+}
+BSARCH_DLL_API(void) bsa_file_flags_set(bsa_archive_t, uint32_t) {}
+BSARCH_DLL_API(bool) bsa_compress_get(bsa_archive_t)
{
return false;
}
-
-BSARCH_DLL_API(void) bsa_share_data_set(bsa_archive_t archive, bool flags)
+BSARCH_DLL_API(void) bsa_compress_set(bsa_archive_t, bool) {}
+BSARCH_DLL_API(bool) bsa_share_data_get(bsa_archive_t)
{
- return;
+ return false;
}
-
+BSARCH_DLL_API(void) bsa_share_data_set(bsa_archive_t, bool) {}
BSARCH_DLL_API(void)
-bsa_file_dds_info_callback_set(bsa_archive_t archive, bsa_file_dds_info_proc_t file_dds_info_proc, void *context)
+bsa_file_dds_info_callback_set(bsa_archive_t, bsa_file_dds_info_proc_t, void*)
{
- return;
}
-} // namespace libbsarch
-#endif
+
+#else // _WIN32
+
+// Windows path: upstream MO2 uses a prebuilt libbsarch DLL from vcpkg; this
+// local tree is a compatibility stub that matches the exported symbol names.
+// Keep it byte-compatible with previous behavior.
+
+BSARCH_DLL_API(bsa_entry_list_t) bsa_entry_list_create() { return nullptr; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_free(bsa_entry_list_t) { return {0}; }
+BSARCH_DLL_API(uint32_t) bsa_entry_list_count(bsa_entry_list_t) { return 0; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_add(bsa_entry_list_t, const wchar_t*) { return {0}; }
+BSARCH_DLL_API(uint32_t) bsa_entry_list_get(bsa_entry_list_t, uint32_t, uint32_t, wchar_t*) { return 0; }
+BSARCH_DLL_API(bsa_archive_t) bsa_create() { return nullptr; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_free(bsa_archive_t) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_load_from_file(bsa_archive_t, const wchar_t*) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_create_archive(bsa_archive_t, const wchar_t*, bsa_archive_type_t, bsa_entry_list_t) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_save(bsa_archive_t) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_add_file_from_disk(bsa_archive_t, const wchar_t*, const wchar_t*) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_add_file_from_disk_root(bsa_archive_t, const wchar_t*, const wchar_t*) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_add_file_from_memory(bsa_archive_t, const wchar_t*, uint32_t, bsa_buffer_t) { return {0}; }
+BSARCH_DLL_API(bsa_file_record_t) bsa_find_file_record(bsa_archive_t, const wchar_t*) { return nullptr; }
+BSARCH_DLL_API(bsa_result_message_buffer_t) bsa_extract_file_data_by_record(bsa_archive_t, bsa_file_record_t) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_buffer_t) bsa_extract_file_data_by_filename(bsa_archive_t, const wchar_t*) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_file_data_free(bsa_archive_t, bsa_result_buffer_t) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_extract_file(bsa_archive_t, const wchar_t*, const wchar_t*) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_iterate_files(bsa_archive_t, bsa_file_iteration_proc_t, void*) { return {0}; }
+BSARCH_DLL_API(bool) bsa_file_exists(bsa_archive_t, const wchar_t*) { return false; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_get_resource_list(bsa_archive_t, bsa_entry_list_t, const wchar_t*) { return {0}; }
+BSARCH_DLL_API(bsa_result_message_t) bsa_close(bsa_archive_t) { return {0}; }
+BSARCH_DLL_API(uint32_t) bsa_filename_get(bsa_archive_t, uint32_t, wchar_t*) { return 0; }
+BSARCH_DLL_API(bsa_archive_type_t) bsa_archive_type_get(bsa_archive_t) { return bsa_archive_type_t::baNone; }
+BSARCH_DLL_API(uint32_t) bsa_version_get(bsa_archive_t) { return 0; }
+BSARCH_DLL_API(uint32_t) bsa_format_name_get(bsa_archive_t, uint32_t, wchar_t*) { return 0; }
+BSARCH_DLL_API(uint32_t) bsa_file_count_get(bsa_archive_t) { return 0; }
+BSARCH_DLL_API(uint32_t) bsa_archive_flags_get(bsa_archive_t) { return 0; }
+BSARCH_DLL_API(void) bsa_archive_flags_set(bsa_archive_t, uint32_t) {}
+BSARCH_DLL_API(uint32_t) bsa_file_flags_get(bsa_archive_t) { return 0; }
+BSARCH_DLL_API(void) bsa_file_flags_set(bsa_archive_t, uint32_t) {}
+BSARCH_DLL_API(bool) bsa_compress_get(bsa_archive_t) { return false; }
+BSARCH_DLL_API(void) bsa_compress_set(bsa_archive_t, bool) {}
+BSARCH_DLL_API(bool) bsa_share_data_get(bsa_archive_t) { return false; }
+BSARCH_DLL_API(void) bsa_share_data_set(bsa_archive_t, bool) {}
+BSARCH_DLL_API(void) bsa_file_dds_info_callback_set(bsa_archive_t, bsa_file_dds_info_proc_t, void*) {}
+
+#endif // !_WIN32
+
+} // namespace libbsarch
+#endif // BSARCH_DLL_EXPORT
diff --git a/libs/preview_bsa/src/previewbsa.cpp b/libs/preview_bsa/src/previewbsa.cpp
index e21df2f..9941f06 100644
--- a/libs/preview_bsa/src/previewbsa.cpp
+++ b/libs/preview_bsa/src/previewbsa.cpp
@@ -19,6 +19,7 @@ along with Bsa Preview plugin. If not, see <http://www.gnu.org/licenses/>.
#include "previewbsa.h"
#include <QApplication>
+#include <QDialog>
#include <QFileInfo>
#include <QImageReader>
#include <QLabel>
@@ -26,6 +27,7 @@ along with Bsa Preview plugin. If not, see <http://www.gnu.org/licenses/>.
#include <QScreen>
#include <QStandardItemModel>
#include <QTextEdit>
+#include <QTimer>
#include <QTreeView>
#include <QTreeWidget>
#include <QVBoxLayout>
@@ -35,8 +37,14 @@ along with Bsa Preview plugin. If not, see <http://www.gnu.org/licenses/>.
#include <uibase/log.h>
#include <uibase/utility.h>
+#include "simplefiletreeitem.h"
#include "simplefiletreemodel.h"
+#include <uibase/imoinfo.h>
+
+#include <memory>
+#include <vector>
+
using namespace MOBase;
PreviewBsa::PreviewBsa() : m_MOInfo(nullptr) {}
@@ -132,29 +140,30 @@ QWidget* PreviewBsa::genBsaPreview(const QString& fileName, const QSize&)
QVBoxLayout* layout = new QVBoxLayout();
QLabel* infoLabel = new QLabel();
- BSA::Archive arch; // bs_archive_auto is easier to use, but is less performant when
- // working with memory
- BSA::EErrorCode res = arch.read(fileName.toLocal8Bit().constData(), true);
+
+ // Kept alive via shared_ptr captured by the double-click lambda below so
+ // we can extract files from the still-open archive on demand without
+ // re-reading it each time.
+ auto archive = std::make_shared<BSA::Archive>();
+ BSA::EErrorCode res =
+ archive->read(fileName.toLocal8Bit().constData(), true);
if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
log::error("invalid bsa '{}', error {}", fileName, res);
infoLabel->setText("Unable to parse archive. Unrecognized format.");
- arch.close();
+ archive->close();
return wrapper;
}
- const BSA::Folder::Ptr archiveDir = arch.getRoot();
+ const BSA::Folder::Ptr archiveDir = archive->getRoot();
readFiles(archiveDir);
QString infoString =
tr("Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , "
"Archive type: %5 , Archive flags: %6");
- // tr("Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , "
- // "Archive type: %5 , Archive flags: %6 , Contents flags: %7");
- infoString = infoString.arg(getFormat(arch.getType()))
- .arg((arch.getFlags() & 0x00000004) ? tr("yes") : tr("no"))
+ infoString = infoString.arg(getFormat(archive->getType()))
+ .arg((archive->getFlags() & 0x00000004) ? tr("yes") : tr("no"))
.arg(m_Files.size())
- .arg(getVersion(arch.getType()))
- .arg(arch.getType())
- .arg("0x" + QString::number(arch.getFlags(), 16));
- //.arg("0x" + QString::number(arch.get_file_flags(), 16));
+ .arg(getVersion(archive->getType()))
+ .arg(archive->getType())
+ .arg("0x" + QString::number(archive->getFlags(), 16));
infoLabel->setText(infoString);
layout->addWidget(infoLabel);
@@ -173,8 +182,50 @@ QWidget* PreviewBsa::genBsaPreview(const QString& fileName, const QSize&)
view->setSortingEnabled(true);
view->sortByColumn(0, Qt::SortOrder::AscendingOrder);
+ // Double-click: extract the selected file from the archive and hand the
+ // raw bytes to another preview plugin via IOrganizer::previewFileData.
+ // The view's model is wrapped in FilterWidgetProxyModel, so walk up via
+ // index.parent() + DisplayRole (proxy-transparent) instead of using
+ // index.internalPointer() directly.
+ IOrganizer* mo = m_MOInfo;
+ QObject::connect(
+ view, &QTreeView::doubleClicked, view,
+ [mo, archive, view](const QModelIndex& index) {
+ if (!index.isValid()) return;
+
+ // Ignore folders — only files have no children.
+ if (index.model()->rowCount(index) > 0) {
+ return;
+ }
+
+ QStringList parts;
+ QModelIndex cur = index;
+ while (cur.isValid()) {
+ parts.prepend(cur.data(Qt::DisplayRole).toString());
+ cur = cur.parent();
+ }
+ const QString path = parts.join('/');
+ if (path.isEmpty()) return;
+
+ BSA::File::Ptr file = archive->findFile(path.toStdString());
+ if (!file) {
+ log::warn("preview_bsa: file not found in archive: {}", path);
+ return;
+ }
+ std::vector<unsigned char> data;
+ if (archive->extractToMemory(file, data) != BSA::ERROR_NONE) {
+ log::warn("preview_bsa: extract failed for: {}", path);
+ return;
+ }
+ QByteArray bytes(reinterpret_cast<const char*>(data.data()),
+ static_cast<int>(data.size()));
+
+ if (!mo->previewFileData(view->window(), path, bytes)) {
+ log::warn("preview_bsa: no preview plugin for: {}", path);
+ }
+ });
+
wrapper->setLayout(layout);
- arch.close();
return wrapper;
}
diff --git a/libs/preview_bsa/src/previewbsa.h b/libs/preview_bsa/src/previewbsa.h
index 6c3ee2e..525d917 100644
--- a/libs/preview_bsa/src/previewbsa.h
+++ b/libs/preview_bsa/src/previewbsa.h
@@ -62,7 +62,7 @@ private:
m_PreviewGenerators;
private:
- const MOBase::IOrganizer* m_MOInfo;
+ MOBase::IOrganizer* m_MOInfo;
QStringList m_Files;
};
diff --git a/libs/preview_bsa/src/simplefiletreeitem.cpp b/libs/preview_bsa/src/simplefiletreeitem.cpp
index 5bcc5d4..23d1241 100644
--- a/libs/preview_bsa/src/simplefiletreeitem.cpp
+++ b/libs/preview_bsa/src/simplefiletreeitem.cpp
@@ -70,3 +70,15 @@ int SimpleFileTreeItem::row() const
return 0;
}
+
+QString SimpleFileTreeItem::fullPath(QChar separator) const
+{
+ // Walk up, collecting names. Skip the root sentinel (parent == nullptr).
+ QStringList parts;
+ const SimpleFileTreeItem* node = this;
+ while (node && node->m_parentItem) {
+ parts.prepend(node->m_itemData.value(0).toString());
+ node = node->m_parentItem;
+ }
+ return parts.join(separator);
+}
diff --git a/libs/preview_bsa/src/simplefiletreeitem.h b/libs/preview_bsa/src/simplefiletreeitem.h
index 3d5ddd7..b3a6eb4 100644
--- a/libs/preview_bsa/src/simplefiletreeitem.h
+++ b/libs/preview_bsa/src/simplefiletreeitem.h
@@ -1,6 +1,7 @@
#ifndef SIMPLEFILETREEITEM_H
#define SIMPLEFILETREEITEM_H
+#include <QString>
#include <QVariant>
#include <QVector>
@@ -22,6 +23,12 @@ public:
int row() const;
SimpleFileTreeItem* parentItem();
+ /**
+ * Walk up to the root, joining each ancestor's name with @p separator.
+ * Returns the archive-relative path of this item.
+ */
+ QString fullPath(QChar separator = QLatin1Char('/')) const;
+
private:
QVector<SimpleFileTreeItem*> m_childItems;
QHash<QString, SimpleFileTreeItem*> m_childItemsByName;
diff --git a/libs/uibase/include/uibase/imoinfo.h b/libs/uibase/include/uibase/imoinfo.h
index a9ec3d8..1725410 100644
--- a/libs/uibase/include/uibase/imoinfo.h
+++ b/libs/uibase/include/uibase/imoinfo.h
@@ -21,11 +21,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#ifndef IMOINFO_H
#define IMOINFO_H
+#include <QByteArray>
#include <QDir>
#include <QList>
#include <QMainWindow>
#include <QString>
#include <QVariant>
+#include <QWidget>
#include <cstdint>
#include <any>
#include <functional>
@@ -370,6 +372,25 @@ public:
virtual IGameFeatures* gameFeatures() const = 0;
/**
+ * @brief show a preview dialog for a file loaded from an in-memory buffer.
+ *
+ * Routes @p fileData through the first registered IPluginPreview that
+ * supports @p fileName's extension and archives. Used by plugins that have
+ * already extracted a blob from a container (e.g. preview_bsa) and want to
+ * hand it off to another preview plugin (e.g. preview_nif) without writing
+ * it to a temp file.
+ *
+ * @param parent parent for the modal preview dialog
+ * @param fileName virtual filename; used for extension dispatch and as the
+ * dialog title
+ * @param fileData raw file bytes
+ * @return true if a preview plugin was found and the dialog was shown;
+ * false otherwise (no suitable plugin / empty data).
+ */
+ virtual bool previewFileData(QWidget* parent, const QString& fileName,
+ const QByteArray& fileData) = 0;
+
+ /**
* @brief runs a program using the virtual filesystem
*
* @param executable either the name of an executable configured in MO, or