diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 17:55:56 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 17:55:56 -0500 |
| commit | 892070f3dec1dc690983fd862880e37e711c2516 (patch) | |
| tree | 423da9fca609a9239efbb6c74ab2e8657975c5a4 /libs | |
| parent | a0355c9b897281d0bfea48bec9d490724ecabd96 (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.h | 21 | ||||
| -rw-r--r-- | libs/bsatk/src/bsaarchive.cpp | 102 | ||||
| -rw-r--r-- | libs/libbsarch/CMakeLists.txt | 16 | ||||
| -rw-r--r-- | libs/libbsarch/src/libbsarch.cpp | 460 | ||||
| -rw-r--r-- | libs/preview_bsa/src/previewbsa.cpp | 79 | ||||
| -rw-r--r-- | libs/preview_bsa/src/previewbsa.h | 2 | ||||
| -rw-r--r-- | libs/preview_bsa/src/simplefiletreeitem.cpp | 12 | ||||
| -rw-r--r-- | libs/preview_bsa/src/simplefiletreeitem.h | 7 | ||||
| -rw-r--r-- | libs/uibase/include/uibase/imoinfo.h | 21 |
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 |
