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
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/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,
}
}
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};
+}