From b3e2eb748a0e58f3efa998ff49a826ad24f97169 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Thu, 8 Aug 2024 14:29:05 -0400 Subject: [PATCH 1/3] feat(vpkpp): add support for Vampire: The Masquerade - Bloodlines VPK files --- include/vpkpp/PackFileType.h | 1 + include/vpkpp/format/VPK_VTMB.h | 32 ++++++ src/vpkpp/PackFile.cpp | 2 + src/vpkpp/_vpkpp.cmake | 2 + src/vpkpp/format/PAK.cpp | 2 +- src/vpkpp/format/VPK_VTMB.cpp | 170 ++++++++++++++++++++++++++++++++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 include/vpkpp/format/VPK_VTMB.h create mode 100644 src/vpkpp/format/VPK_VTMB.cpp diff --git a/include/vpkpp/PackFileType.h b/include/vpkpp/PackFileType.h index 4c1564800..9037e92b0 100644 --- a/include/vpkpp/PackFileType.h +++ b/include/vpkpp/PackFileType.h @@ -12,6 +12,7 @@ enum class PackFileType { PAK, PCK, VPK, + VPK_VTMB, ZIP, }; diff --git a/include/vpkpp/format/VPK_VTMB.h b/include/vpkpp/format/VPK_VTMB.h new file mode 100644 index 000000000..b9ef37428 --- /dev/null +++ b/include/vpkpp/format/VPK_VTMB.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "../PackFile.h" + +namespace vpkpp { + +constexpr std::string_view VPK_VTMB_EXTENSION = ".vpk"; + +class VPK_VTMB : public PackFile { +public: + /// Open Vampire: The Masquerade - Bloodlines VPK files + [[nodiscard]] static std::unique_ptr open(const std::string& path, PackFileOptions options = {}, const Callback& callback = nullptr); + + [[nodiscard]] std::optional> readEntry(const Entry& entry) const override; + + bool bake(const std::string& outputDir_ /*= ""*/, const Callback& callback /*= nullptr*/) override; + + [[nodiscard]] std::vector getSupportedEntryAttributes() const override; + +protected: + VPK_VTMB(const std::string& fullFilePath_, PackFileOptions options_); + + Entry& addEntryInternal(Entry& entry, const std::string& filename_, std::vector& buffer, EntryOptions options_) override; + +private: + VPKPP_REGISTER_PACKFILE_OPEN(VPK_VTMB_EXTENSION, &VPK_VTMB::open); +}; + +} // namespace vpkpp diff --git a/src/vpkpp/PackFile.cpp b/src/vpkpp/PackFile.cpp index 1db4de0a0..fddc465ae 100644 --- a/src/vpkpp/PackFile.cpp +++ b/src/vpkpp/PackFile.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include using namespace sourcepp; @@ -418,6 +419,7 @@ bool PackFile::extractAll(const std::string& outputDir, const std::function rootDirList; { std::vector> pathSplits; + pathSplits.reserve(saveEntries.size()); for (const auto& entry : saveEntries) { pathSplits.push_back(::splitPath(entry.path)); } diff --git a/src/vpkpp/_vpkpp.cmake b/src/vpkpp/_vpkpp.cmake index e3446d328..efd93eb86 100644 --- a/src/vpkpp/_vpkpp.cmake +++ b/src/vpkpp/_vpkpp.cmake @@ -9,6 +9,7 @@ add_pretty_parser(vpkpp "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/PAK.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/PCK.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/VPK.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/VPK_VTMB.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/ZIP.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/Attribute.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/Entry.h" @@ -25,6 +26,7 @@ add_pretty_parser(vpkpp "${CMAKE_CURRENT_LIST_DIR}/format/PAK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PCK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK.cpp" + "${CMAKE_CURRENT_LIST_DIR}/format/VPK_VTMB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/ZIP.cpp" "${CMAKE_CURRENT_LIST_DIR}/Entry.cpp" "${CMAKE_CURRENT_LIST_DIR}/PackFile.cpp") diff --git a/src/vpkpp/format/PAK.cpp b/src/vpkpp/format/PAK.cpp index fe2d7daac..5e6dcc346 100644 --- a/src/vpkpp/format/PAK.cpp +++ b/src/vpkpp/format/PAK.cpp @@ -37,7 +37,7 @@ std::unique_ptr PAK::open(const std::string& path, PackFileOptions opt auto fileCount = reader.read() / 64; reader.seek_in(directoryOffset); - for (int i = 0; i < fileCount; i++) { + for (uint32_t i = 0; i < fileCount; i++) { Entry entry = createNewEntry(); reader.read(entry.path, PAK_FILENAME_MAX_SIZE); diff --git a/src/vpkpp/format/VPK_VTMB.cpp b/src/vpkpp/format/VPK_VTMB.cpp new file mode 100644 index 000000000..cf5320ef5 --- /dev/null +++ b/src/vpkpp/format/VPK_VTMB.cpp @@ -0,0 +1,170 @@ +#include + +#include + +#include + +#include +#include +#include + +using namespace sourcepp; +using namespace vpkpp; + +VPK_VTMB::VPK_VTMB(const std::string& fullFilePath_, PackFileOptions options_) + : PackFile(fullFilePath_, options_) { + this->type = PackFileType::VPK_VTMB; +} + +std::unique_ptr VPK_VTMB::open(const std::string& path, PackFileOptions options, const Callback& callback) { + if (!std::filesystem::exists(path)) { + // File does not exist + return nullptr; + } + + // Extra check to make sure this is a VTMB VPK path + auto stem = std::filesystem::path{path}.stem().string(); + if (stem.length() != 7 || !stem.starts_with("pack") || !parser::text::isNumber(stem.substr(4))) { + return nullptr; + } + + auto* vpkVTMB = new VPK_VTMB{path, options}; + auto packFile = std::unique_ptr(vpkVTMB); + + FileStream reader{vpkVTMB->fullFilePath}; + reader.seek_in(-static_cast(sizeof(uint32_t) * 2 + sizeof(uint8_t)), std::ios::end); + + auto fileCount = reader.read(); + auto dirOffset = reader.read(); + + // Make 100% sure + auto version = reader.read(); + if (version != 0) { + return nullptr; + } + + // Ok now let's load this thing + reader.seek_in(dirOffset); + for (uint32_t i = 0; i < fileCount; i++) { + Entry entry = createNewEntry(); + + entry.path = reader.read_string(reader.read()); + string::normalizeSlashes(entry.path, true); + if (!vpkVTMB->isCaseSensitive()) { + string::toLower(entry.path); + } + + entry.offset = reader.read(); + entry.length = reader.read(); + + auto parentDir = std::filesystem::path{entry.path}.parent_path().string(); + if (!vpkVTMB->entries.contains(parentDir)) { + vpkVTMB->entries[parentDir] = {}; + } + vpkVTMB->entries[parentDir].push_back(entry); + + if (callback) { + callback(parentDir, entry); + } + } + + return packFile; +} + +std::optional> VPK_VTMB::readEntry(const Entry& entry) const { + if (entry.unbaked) { + return this->readUnbakedEntry(entry); + } + + // It's baked into the file on disk + FileStream stream{this->fullFilePath}; + if (!stream) { + return std::nullopt; + } + stream.seek_in_u(entry.offset); + return stream.read_bytes(entry.length); +} + +Entry& VPK_VTMB::addEntryInternal(Entry& entry, const std::string& filename_, std::vector& buffer, EntryOptions options_) { + auto filename = filename_; + if (!this->isCaseSensitive()) { + string::toLower(filename); + } + auto [dir, name] = splitFilenameAndParentDir(filename); + + entry.path = filename; + entry.length = buffer.size(); + + // Offset will be reset when it's baked + entry.offset = 0; + + if (!this->unbakedEntries.contains(dir)) { + this->unbakedEntries[dir] = {}; + } + this->unbakedEntries.at(dir).push_back(entry); + return this->unbakedEntries.at(dir).back(); +} + +bool VPK_VTMB::bake(const std::string& outputDir_, const Callback& callback) { + // Get the proper file output folder + std::string outputDir = this->getBakeOutputDir(outputDir_); + std::string outputPath = outputDir + '/' + this->getFilename(); + + // Reconstruct data for ease of access + std::vector entriesToBake; + for (auto& [entryDir, entryList] : this->entries) { + for (auto& entry : entryList) { + entriesToBake.push_back(&entry); + } + } + for (auto& [entryDir, entryList] : this->unbakedEntries) { + for (auto& entry : entryList) { + entriesToBake.push_back(&entry); + } + } + + { + FileStream stream{outputPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT}; + stream.seek_out(0); + + // File data + for (auto* entry : entriesToBake) { + if (auto binData = this->readEntry(*entry)) { + entry->offset = stream.tell_out(); + + stream.write(*binData); + } else { + entry->offset = 0; + entry->length = 0; + } + } + + // Directory + auto dirOffset = stream.tell_out(); + for (auto entry : entriesToBake) { + stream.write(entry->path.length()); + stream.write(entry->path, false); + stream.write(entry->offset); + stream.write(entry->length); + + if (callback) { + callback(entry->getParentPath(), *entry); + } + } + + // Footer + stream.write(this->entries.size() + this->unbakedEntries.size()); + stream.write(dirOffset); + stream.write(0); + } + + // Clean up + this->mergeUnbakedEntries(); + PackFile::setFullFilePath(outputDir); + return true; +} + +std::vector VPK_VTMB::getSupportedEntryAttributes() const { + using enum Attribute; + return {LENGTH}; +} From a82ccedf73e5ecf73b307566c33346ccd7e4e6d8 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Thu, 8 Aug 2024 15:03:04 -0400 Subject: [PATCH 2/3] chore: add vpk vtmb to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f9164877..7c436942c 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
  • PAK (Quake, WON Half-Life)
  • PCK v1-2 (Godot Engine)
  • VPK v1-2
  • +
  • VPK (Vampire: The Masquerade - Bloodlines)
  • ZIP
  • From 05e9331a549693375eb872412c4d2e55e86ac66b Mon Sep 17 00:00:00 2001 From: craftablescience Date: Thu, 8 Aug 2024 15:09:07 -0400 Subject: [PATCH 3/3] feat(vpkpp): update lang wrappers with VTMB vpks --- lang/c/include/vpkppc/PackFileType.h | 1 + lang/c/include/vpkppc/format/VPK_VTMB.h | 9 ++++++ lang/c/src/vpkppc/_vpkppc.cmake | 2 ++ lang/c/src/vpkppc/format/VPK_VTMB.cpp | 28 ++++++++++++++++++ lang/csharp/src/vpkpp/Format/VPK_VTMB.cs | 36 ++++++++++++++++++++++++ lang/csharp/src/vpkpp/PackFileType.cs | 1 + 6 files changed, 77 insertions(+) create mode 100644 lang/c/include/vpkppc/format/VPK_VTMB.h create mode 100644 lang/c/src/vpkppc/format/VPK_VTMB.cpp create mode 100644 lang/csharp/src/vpkpp/Format/VPK_VTMB.cs diff --git a/lang/c/include/vpkppc/PackFileType.h b/lang/c/include/vpkppc/PackFileType.h index 9069dc744..fe7baa1ff 100644 --- a/lang/c/include/vpkppc/PackFileType.h +++ b/lang/c/include/vpkppc/PackFileType.h @@ -14,6 +14,7 @@ typedef enum { VPKPP_PACK_FILE_TYPE_PAK, VPKPP_PACK_FILE_TYPE_PCK, VPKPP_PACK_FILE_TYPE_VPK, + VPKPP_PACK_FILE_TYPE_VPK_VTMB, VPKPP_PACK_FILE_TYPE_ZIP, } vpkpp_pack_file_type_e; diff --git a/lang/c/include/vpkppc/format/VPK_VTMB.h b/lang/c/include/vpkppc/format/VPK_VTMB.h new file mode 100644 index 000000000..504c69ab9 --- /dev/null +++ b/lang/c/include/vpkppc/format/VPK_VTMB.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../PackFile.h" + +// REQUIRES MANUAL FREE: vpkpp_close +SOURCEPP_API vpkpp_pack_file_handle_t vpkpp_vpk_vtmb_open(const char* path); + +// REQUIRES MANUAL FREE: vpkpp_close +SOURCEPP_API vpkpp_pack_file_handle_t vpkpp_vpk_vtmb_open_with_options(const char* path, vpkpp_pack_file_options_t options); diff --git a/lang/c/src/vpkppc/_vpkppc.cmake b/lang/c/src/vpkppc/_vpkppc.cmake index 3a4d00707..04fb5855c 100644 --- a/lang/c/src/vpkppc/_vpkppc.cmake +++ b/lang/c/src/vpkppc/_vpkppc.cmake @@ -8,6 +8,7 @@ add_pretty_parser(vpkpp C "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/PAK.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/PCK.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/VPK.h" + "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/VPK_VTMB.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/ZIP.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/Attribute.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/Entry.h" @@ -25,6 +26,7 @@ add_pretty_parser(vpkpp C "${CMAKE_CURRENT_LIST_DIR}/format/PAK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PCK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK.cpp" + "${CMAKE_CURRENT_LIST_DIR}/format/VPK_VTMB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/ZIP.cpp" "${CMAKE_CURRENT_LIST_DIR}/Convert.cpp" "${CMAKE_CURRENT_LIST_DIR}/Entry.cpp" diff --git a/lang/c/src/vpkppc/format/VPK_VTMB.cpp b/lang/c/src/vpkppc/format/VPK_VTMB.cpp new file mode 100644 index 000000000..e5bba5b79 --- /dev/null +++ b/lang/c/src/vpkppc/format/VPK_VTMB.cpp @@ -0,0 +1,28 @@ +#include + +#include + +#include +#include + +using namespace vpkpp; + +SOURCEPP_API vpkpp_pack_file_handle_t vpkpp_vpk_vtmb_open(const char* path) { + SOURCEPP_EARLY_RETURN_VAL(path, nullptr); + + auto packFile = VPK_VTMB::open(path); + if (!packFile) { + return nullptr; + } + return packFile.release(); +} + +SOURCEPP_API vpkpp_pack_file_handle_t vpkpp_vpk_vtmb_open_with_options(const char* path, vpkpp_pack_file_options_t options) { + SOURCEPP_EARLY_RETURN_VAL(path, nullptr); + + auto packFile = VPK_VTMB::open(path, Convert::optionsFromC(options)); + if (!packFile) { + return nullptr; + } + return packFile.release(); +} diff --git a/lang/csharp/src/vpkpp/Format/VPK_VTMB.cs b/lang/csharp/src/vpkpp/Format/VPK_VTMB.cs new file mode 100644 index 000000000..45be95d14 --- /dev/null +++ b/lang/csharp/src/vpkpp/Format/VPK_VTMB.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices; + +namespace vpkpp.Format +{ + internal static unsafe partial class Extern + { + [DllImport("vpkppc")] + public static extern void* vpkpp_vpk_vtmb_open([MarshalAs(UnmanagedType.LPStr)] string path); + + [DllImport("vpkppc")] + public static extern void* vpkpp_vpk_vtmb_open_with_options([MarshalAs(UnmanagedType.LPStr)] string path, PackFileOptions options); + } + + public class VPK_VTMB : PackFile + { + private protected unsafe VPK_VTMB(void* handle) : base(handle) {} + + public new static VPK_VTMB? Open(string path) + { + unsafe + { + var handle = Extern.vpkpp_vpk_vtmb_open(path); + return handle == null ? null : new VPK_VTMB(handle); + } + } + + public new static VPK_VTMB? Open(string path, PackFileOptions options) + { + unsafe + { + var handle = Extern.vpkpp_vpk_vtmb_open_with_options(path, options); + return handle == null ? null : new VPK_VTMB(handle); + } + } + } +} diff --git a/lang/csharp/src/vpkpp/PackFileType.cs b/lang/csharp/src/vpkpp/PackFileType.cs index e79e331b5..8225b07f8 100644 --- a/lang/csharp/src/vpkpp/PackFileType.cs +++ b/lang/csharp/src/vpkpp/PackFileType.cs @@ -11,6 +11,7 @@ public enum PackFileType PAK, PCK, VPK, + VPK_VTMB, ZIP, } }