17#ifdef VPKPP_SUPPORT_VPK_V54
30std::string removeVPKAndOrDirSuffix(
const std::string& path,
bool isFPX) {
31 std::string filename = path;
33 filename = filename.substr(0, filename.length() - 4);
39 filename = filename.substr(0, filename.length() - 4);
45bool isFPX(
const VPK* vpk) {
46 return dynamic_cast<const FPX*
>(vpk);
51std::unique_ptr<PackFile>
VPK::create(
const std::string& path, uint32_t version) {
52 if (version != 0 && version != 1 && version != 2 && version != 54) {
57 FileStream stream{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
68 header2.fileDataSectionSize = 0;
69 header2.archiveMD5SectionSize = 0;
70 header2.otherMD5SectionSize = 0;
71 header2.signatureSectionSize = 0;
81 std::unique_ptr<PackFile> vpk;
84 if (path.length() >= 8) {
85 auto dirPath = path.substr(0, path.length() - 8) +
"_dir.vpk";
86 auto pathEnd = path.substr(path.length() - 8, path.length());
87 if (
string::matches(pathEnd,
"_%d%d%d.vpk") && std::filesystem::exists(dirPath)) {
99 if (!std::filesystem::exists(path)) {
104 auto* vpk =
new VPK{path};
105 auto packFile = std::unique_ptr<PackFile>{vpk};
107 FileStream reader{vpk->fullFilePath};
109 reader.read(vpk->header1);
111 reader.seek_in(3, std::ios::end);
112 if (reader.read<
char>() ==
'\0' && reader.read<
char>() ==
'\0' && reader.read<
char>() ==
'\0') {
115 if (std::filesystem::file_size(vpk->fullFilePath) == 9) {
121 vpk->header1.version = 0;
122 vpk->header1.treeSize = 0;
130 if (vpk->hasExtendedHeader()) {
131 reader.read(vpk->header2);
132 }
else if (vpk->header1.version != 0 && vpk->header1.version != 1) {
139 std::string extension;
140 reader.read(extension);
141 if (extension.empty())
146 std::string directory;
147 reader.read(directory);
148 if (directory.empty())
152 if (directory ==
" ") {
160 std::string entryName;
161 reader.read(entryName);
162 if (entryName.empty())
167 std::string entryPath;
168 if (extension ==
" ") {
169 entryPath = fullDir.empty() ?
"" : fullDir +
'/';
170 entryPath += entryName;
172 entryPath = fullDir.empty() ?
"" : fullDir +
'/';
173 entryPath += entryName +
'.';
174 entryPath += extension;
176 entryPath = vpk->cleanEntryPath(entryPath);
178 reader.read(entry.
crc32);
179 auto preloadedDataSize = reader.read<uint16_t>();
181 entry.
offset = reader.read<uint32_t>();
182 entry.
length = reader.read<uint32_t>();
184 if (vpk->hasCompression()) {
193 if (preloadedDataSize > 0) {
194 entry.
extraData = reader.read_bytes(preloadedDataSize);
195 entry.
length += preloadedDataSize;
199 vpk->numArchives =
static_cast<int32_t
>(entry.
archiveIndex);
203 vpk->entries.emplace(entryPath, entry);
206 callback(entryPath, entry);
216 if (!vpk->hasExtendedHeader()) {
221 reader.seek_in(vpk->header2.fileDataSectionSize, std::ios::cur);
223 if (vpk->header2.archiveMD5SectionSize %
sizeof(
MD5Entry) != 0) {
227 vpk->md5Entries.clear();
228 unsigned int entryNum = vpk->header2.archiveMD5SectionSize /
sizeof(
MD5Entry);
229 for (
unsigned int i = 0; i < entryNum; i++) {
230 vpk->md5Entries.push_back(reader.read<
MD5Entry>());
233 if (vpk->header2.otherMD5SectionSize != 48) {
238 vpk->footer2.treeChecksum = reader.read_bytes<16>();
239 vpk->footer2.md5EntriesChecksum = reader.read_bytes<16>();
240 vpk->footer2.wholeFileChecksum = reader.read_bytes<16>();
242 if (!vpk->header2.signatureSectionSize) {
246 auto publicKeySize = reader.read<int32_t>();
247 if (vpk->header2.signatureSectionSize == 20 && publicKeySize ==
VPK_SIGNATURE) {
252 vpk->footer2.publicKey = reader.read_bytes(publicKeySize);
253 vpk->footer2.signature = reader.read_bytes(reader.read<int32_t>());
285 if (this->
footer2.wholeFileChecksum !=
crypto::computeMD5(stream.read_bytes(this->getHeaderLength() + this->header1.treeSize + this->header2.fileDataSectionSize + this->header2.archiveMD5SectionSize + this->header2.otherMD5SectionSize -
sizeof(this->footer2.wholeFileChecksum)))) {
296 if (this->
footer2.publicKey.empty() || this->footer2.signature.empty()) {
308 if (this->
footer2.publicKey.empty() || this->footer2.signature.empty()) {
312 const auto signatureSectionSize = this->
footer2.publicKey.size() + this->
footer2.signature.size() +
sizeof(uint32_t) * 2;
313 if (dirFileBuffer.size() <= signatureSectionSize) {
316 dirFileBuffer.resize(dirFileBuffer.size() - signatureSectionSize);
321std::optional<std::vector<std::byte>>
VPK::readEntry(
const std::string& path_)
const {
327 if (entry->unbaked) {
331 const auto entryLength = (this->
hasCompression() && entry->compressedLength) ? entry->compressedLength : entry->length;
332 if (entryLength == 0) {
333 return std::vector<std::byte>{};
335 std::vector out(entryLength,
static_cast<std::byte
>(0));
337 if (!entry->extraData.empty()) {
338 std::copy(entry->extraData.begin(), entry->extraData.end(), out.begin());
340 if (entryLength != entry->extraData.size()) {
347 stream.seek_in_u(entry->offset);
348 auto bytes = stream.read_bytes(entryLength - entry->extraData.size());
349 std::copy(bytes.begin(), bytes.end(), out.begin() +
static_cast<long long>(entry->extraData.size()));
357 auto bytes = stream.read_bytes(entry->length - entry->extraData.size());
358 std::copy(bytes.begin(), bytes.end(), out.begin() +
static_cast<long long>(entry->extraData.size()));
362#ifndef VPKPP_SUPPORT_VPK_V54
370 if (!decompressionDict) {
374 std::unique_ptr<ZSTD_DDict, void(*)(
void*)> dDict{
375 ZSTD_createDDict(decompressionDict->data(), decompressionDict->size()),
376 [](
void* dDict_) { ZSTD_freeDDict(
static_cast<ZSTD_DDict*
>(dDict_)); },
382 std::unique_ptr<ZSTD_DCtx, void(*)(
void*)> dCtx{
384 [](
void* dCtx_) { ZSTD_freeDCtx(
static_cast<ZSTD_DCtx*
>(dCtx_)); },
390 std::vector<std::byte> decompressedData;
391 decompressedData.resize(entry->length);
393 if (ZSTD_isError(ZSTD_decompress_usingDDict(dCtx.get(), decompressedData.data(), decompressedData.size(), out.data(), out.size(), dDict.get()))) {
396 return decompressedData;
407 entry.
length = buffer.size();
414 if (!options.
vpk_saveToDirectory && !this->freedChunks.empty() && !this->hasCompression()) {
415 int64_t bestChunkIndex = -1;
416 std::size_t currentChunkGap = SIZE_MAX;
417 for (int64_t i = 0; i < this->
freedChunks.size(); i++) {
420 (bestChunkIndex >= 0 && this->freedChunks[i].length >= entry.
length && (this->freedChunks[i].length - entry.
length) < currentChunkGap)
426 if (bestChunkIndex >= 0) {
431 if (currentChunkGap < SIZE_MAX && currentChunkGap > 0) {
440 entry.
extraData.resize(clampedPreloadBytes);
441 std::memcpy(entry.
extraData.data(), buffer.data(), clampedPreloadBytes);
442 buffer.erase(buffer.begin(), buffer.begin() + clampedPreloadBytes);
459 this->
freedChunks.push_back({entry->offset, entry->length, entry->archiveIndex});
466 if (!dirName.empty()) {
480 std::string outputPath = outputDir +
'/' + this->
getFilename();
482#ifdef VPKPP_SUPPORT_VPK_V54
484 std::optional<std::vector<std::byte>> compressionDict;
485 std::unique_ptr<ZSTD_CDict, void(*)(
void*)> cDict{
nullptr,
nullptr};
486 std::unique_ptr<ZSTD_CCtx, void(*)(
void*)> cCtx{
nullptr,
nullptr};
489 if (!compressionDict) {
495 [](
void* cDict_) { ZSTD_freeCDict(
static_cast<ZSTD_CDict*
>(cDict_)); },
503 [](
void* cCtx_) { ZSTD_freeCCtx(
static_cast<ZSTD_CCtx*
>(cCtx_)); },
512 std::unordered_map<std::string, std::unordered_map<std::string, std::vector<std::pair<std::string, Entry*>>>> temp;
514 const auto fsPath = std::filesystem::path{path};
515 auto extension = fsPath.extension().string();
516 if (extension.starts_with(
'.')) {
517 extension = extension.substr(1);
519 const auto parentDir = fsPath.parent_path().string();
521 if (extension.empty()) {
524 if (!temp.contains(extension)) {
525 temp[extension] = {};
527 if (!temp.at(extension).contains(parentDir)) {
528 temp.at(extension)[parentDir] = {};
530 temp.at(extension).at(parentDir).emplace_back(path, &entry);
534 std::vector<std::byte> dirVPKEntryData;
535 std::size_t newDirEntryOffset = 0;
545 dirVPKEntryData.reserve(dirVPKEntryData.size() + entry.
length - entry.
extraData.size());
546 dirVPKEntryData.insert(dirVPKEntryData.end(), binData->begin() +
static_cast<std::vector<std::byte>::difference_type
>(entry.
extraData.size()), binData->end());
548 entry.
offset = newDirEntryOffset;
553 const auto getArchiveFilename = [
this](
const std::string& filename_, uint32_t archiveIndex) {
560 if (!outputDir_.empty()) {
561 for (uint32_t archiveIndex = 0; archiveIndex < this->
numArchives; archiveIndex++) {
563 if (!std::filesystem::exists(from)) {
566 std::string dest = getArchiveFilename(outputDir +
'/' + this->
getTruncatedFilestem(), archiveIndex);
570 std::filesystem::copy_file(from, dest, std::filesystem::copy_options::overwrite_existing);
574 FileStream outDir{outputPath, FileStream::OPT_READ | FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
579 if (this->
header1.version > 0) {
587 for (
auto& [ext, dirs] : temp) {
590 for (
auto& [dir, tempEntries] : dirs) {
591 outDir.write(!dir.empty() ? dir :
" ");
593 for (
auto& [path, entry] : tempEntries) {
601 if (entry->
length == entry->
extraData.size() && !this->hasCompression()) {
604 entry->
offset = dirVPKEntryData.size();
607 auto archiveFilename = getArchiveFilename(::removeVPKAndOrDirSuffix(outputPath, ::isFPX(
this)), entry->
archiveIndex);
608 FileStream stream{archiveFilename, FileStream::OPT_READ | FileStream::OPT_WRITE | FileStream::OPT_CREATE_IF_NONEXISTENT};
609 stream.seek_out_u(entry->
offset);
610 stream.write(*entryData);
613 auto archiveFilename = getArchiveFilename(::removeVPKAndOrDirSuffix(outputPath, ::isFPX(
this)), entry->
archiveIndex);
614 entry->
offset = std::filesystem::exists(archiveFilename) ? std::filesystem::file_size(archiveFilename) : 0;
615 FileStream stream{archiveFilename, FileStream::OPT_APPEND | FileStream::OPT_CREATE_IF_NONEXISTENT};
616#ifndef VPKPP_SUPPORT_VPK_V54
617 stream.write(*entryData);
620 stream.write(*entryData);
622 std::vector<std::byte> compressedData;
623 compressedData.resize(ZSTD_compressBound(entryData->size()));
624 auto compressedSize = ZSTD_compress_usingCDict(cCtx.get(), compressedData.data(), compressedData.size(), entryData->data(), entryData->size(), cDict.get());
625 if (ZSTD_isError(compressedSize) || compressedData.size() < compressedSize) {
628 stream.write(std::span{compressedData.data(), compressedSize});
634 entry->
offset = dirVPKEntryData.size();
635#ifndef VPKPP_SUPPORT_VPK_V54
636 dirVPKEntryData.insert(dirVPKEntryData.end(), entryData->data(), entryData->data() + entryData->size());
639 dirVPKEntryData.insert(dirVPKEntryData.end(), entryData->data(), entryData->data() + entryData->size());
641 std::vector<std::byte> compressedData;
642 compressedData.resize(ZSTD_compressBound(entryData->size()));
643 auto compressedSize = ZSTD_compress_usingCDict(cCtx.get(), compressedData.data(), compressedData.size(), entryData->data(), entryData->size(), cDict.get());
644 if (ZSTD_isError(compressedSize) || compressedData.size() < compressedSize) {
647 dirVPKEntryData.insert(dirVPKEntryData.end(), compressedData.data(), compressedData.data() + compressedSize);
657 outDir.write(std::filesystem::path{path}.stem().
string());
658 outDir.write(entry->
crc32);
659 outDir.write<uint16_t>(entry->
extraData.size());
661 outDir.write<uint32_t>(entry->
offset);
675 callback(path, *entry);
685 if (!dirVPKEntryData.empty()) {
686 outDir.write(dirVPKEntryData);
701 const auto binData = this->
readEntry(path);
707 .offset =
static_cast<uint32_t
>(entry.
offset),
708 .length =
static_cast<uint32_t
>(entry.
length - entry.
extraData.size()),
716 this->
header2.fileDataSectionSize = dirVPKEntryData.size();
718 this->
header2.otherMD5SectionSize = 48;
719 this->
header2.signatureSectionSize = 0;
722 hash_state wholeFileChecksumMD5;
723 md5_init(&wholeFileChecksumMD5);
726 md5_process(&wholeFileChecksumMD5,
reinterpret_cast<const unsigned char*
>(&this->
header1),
sizeof(this->
header1));
727 md5_process(&wholeFileChecksumMD5,
reinterpret_cast<const unsigned char*
>(&this->
header2),
sizeof(this->
header2));
731 if (this->
header1.treeSize > 0) {
732 std::vector<std::byte> treeData = outDir.read_bytes(this->
header1.treeSize);
733 md5_process(&wholeFileChecksumMD5,
reinterpret_cast<const unsigned char*
>(treeData.data()), treeData.size());
736 this->
footer2.treeChecksum = {};
739 if (!dirVPKEntryData.empty()) {
740 md5_process(&wholeFileChecksumMD5,
reinterpret_cast<const unsigned char*
>(dirVPKEntryData.data()), dirVPKEntryData.size());
744 md5_process(&wholeFileChecksumMD5,
reinterpret_cast<const unsigned char*
>(this->
md5Entries.data()), this->md5Entries.size() *
sizeof(
MD5Entry));
747 this->
footer2.md5EntriesChecksum = {};
750 if (!this->
footer2.treeChecksum.empty()) {
751 md5_process(&wholeFileChecksumMD5,
reinterpret_cast<const unsigned char*
>(this->
footer2.treeChecksum.data()), this->footer2.treeChecksum.size());
753 if (!this->
footer2.md5EntriesChecksum.empty()) {
754 md5_process(&wholeFileChecksumMD5,
reinterpret_cast<const unsigned char*
>(this->
footer2.md5EntriesChecksum.data()), this->footer2.md5EntriesChecksum.size());
756 md5_done(&wholeFileChecksumMD5,
reinterpret_cast<unsigned char*
>(this->
footer2.wholeFileChecksum.data()));
759 this->
footer2.publicKey.clear();
760 this->
footer2.signature.clear();
764 if (this->
header1.version == 0) {
782 outDir.seek_out_u(
sizeof(
Header1) +
sizeof(
Header2) + this->
header1.treeSize + dirVPKEntryData.size());
784 outDir.write(this->
footer2.treeChecksum);
785 outDir.write(this->
footer2.md5EntriesChecksum);
786 outDir.write(this->
footer2.wholeFileChecksum);
797 filestem = filestem.substr(0, filestem.length() - 4);
807VPK::operator std::string()
const {
808 return PackFile::operator std::string() + std::format(
" | Version v{}", this->
header1.version);
814 auto privateKeyPath = name +
".privatekey.vdf";
815 FileStream stream{privateKeyPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
824 stream.write(output,
false);
827 auto publicKeyPath = name +
".publickey.vdf";
828 FileStream stream{publicKeyPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
837 stream.write(output,
false);
843 if (!this->
hasExtendedHeader() || !std::filesystem::exists(filename_) || std::filesystem::is_directory(filename_)) {
849 const auto privateKeyHex = fileKV[
"private_key"][
"rsa_private_key"].
getValue();
850 if (privateKeyHex.empty()) {
853 const auto publicKeyHex = fileKV[
"private_key"][
"public_key"][
"rsa_public_key"].
getValue();
854 if (publicKeyHex.empty()) {
861bool VPK::sign(
const std::vector<std::byte>& privateKey,
const std::vector<std::byte>& publicKey) {
866 this->
header2.signatureSectionSize = this->
footer2.publicKey.size() + this->
footer2.signature.size() +
sizeof(uint32_t) * 2;
868 FileStream stream{std::string{this->
getFilepath()}, FileStream::OPT_READ | FileStream::OPT_WRITE};
869 stream.seek_out(
sizeof(
Header1));
874 if (dirFileBuffer.size() <= this->header2.signatureSectionSize) {
877 for (
int i = 0; i < this->
header2.signatureSectionSize; i++) {
878 dirFileBuffer.pop_back();
880 this->
footer2.publicKey = publicKey;
884 FileStream stream{std::string{this->
getFilepath()}, FileStream::OPT_READ | FileStream::OPT_WRITE};
885 stream.seek_out(this->
getHeaderLength() + this->
header1.treeSize + this->header2.fileDataSectionSize + this->header2.archiveMD5SectionSize + this->header2.otherMD5SectionSize);
886 stream.write(
static_cast<uint32_t
>(this->
footer2.publicKey.size()));
887 stream.write(this->
footer2.publicKey);
888 stream.write(
static_cast<uint32_t
>(this->
footer2.signature.size()));
889 stream.write(this->
footer2.signature);
900 if ((version != 0 && version != 1 && version != 2 && version != 54) || ::isFPX(
this) || version == this->
header1.version) {
903 this->
header1.version = version;
924 return this->
header1.version == 54;
constexpr uint32_t VPK_FLAG_REUSING_CHUNK
Runtime-only flag that indicates a file is going to be written to an existing archive file.
std::string_view getValue() const
Get the value associated with the element.
This class represents the metadata that a file has inside a PackFile.
bool unbaked
Used to check if entry is saved to disk.
uint32_t flags
Format-specific flags (PCK: File flags, VPK: Internal parser state, ZIP: Compression method / strengt...
uint64_t offset
Offset, format-specific meaning - 0 if unused, or if the offset genuinely is 0.
uint64_t compressedLength
If the format supports compression, this is the compressed length.
uint32_t archiveIndex
Which external archive this entry is in.
uint32_t crc32
CRC32 checksum - 0 if unused.
uint64_t length
Length in bytes (in formats with compression, this is the uncompressed length).
std::vector< std::byte > extraData
Format-specific (PCK: MD5 hash, VPK: Preloaded data).
EntryCallbackBase< void > EntryCallback
virtual std::size_t removeDirectory(const std::string &dirName_)
Remove a directory.
void mergeUnbakedEntries()
std::optional< Entry > findEntry(const std::string &path_, bool includeUnbaked=true) const
Try to find an entry given the file path.
std::vector< std::string > verifyEntryChecksumsUsingCRC32() const
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
std::string getFilestem() const
/home/user/pak01_dir.vpk -> pak01_dir
bool bake()
If output folder is an empty string, it will overwrite the original.
std::string getFilename() const
/home/user/pak01_dir.vpk -> pak01_dir.vpk
std::string getBakeOutputDir(const std::string &outputDir) const
std::string getTruncatedFilepath() const
/home/user/pak01_dir.vpk -> /home/user/pak01
void runForAllEntries(const EntryCallback &operation, bool includeUnbaked=true) const
Run a callback for each entry in the pack file.
void setFullFilePath(const std::string &outputDir)
std::string cleanEntryPath(const std::string &path) const
static Entry createNewEntry()
virtual bool removeEntry(const std::string &path_)
Remove an entry.
std::string_view getFilepath() const
/home/user/pak01_dir.vpk
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
static std::unique_ptr< PackFile > create(const std::string &path, uint32_t version=2)
Create a new directory VPK file - should end in "_dir.vpk"! This is not enforced but STRONGLY recomme...
std::size_t removeDirectory(const std::string &dirName_) override
Remove a directory.
void setChunkSize(uint32_t newChunkSize)
Set the VPK chunk size in bytes (size of generated archives when baking).
uint32_t getHeaderLength() const
bool hasCompression() const
bool verifyPackFileSignature() const override
Verify the file signature, returns true on success Will return true if there is no signature ability ...
uint32_t getChunkSize() const
Get the VPK chunk size in bytes (size of generated archives when baking).
std::vector< std::string > verifyEntryChecksums() const override
Verify the checksums of each file, if a file fails the check its path will be added to the vector If ...
uint32_t getVersion() const
Returns 1 for v1, 2 for v2.
uint32_t currentlyFilledChunkSize
bool hasPackFileSignature() const override
Returns true if the file is signed.
bool hasExtendedHeader() const
static bool generateKeyPairFiles(const std::string &name)
Generate keypair files, which can be used to sign a VPK Input is a truncated file path,...
std::vector< FreedChunk > freedChunks
bool hasPackFileChecksum() const override
Returns true if the entire file has a checksum.
bool verifyPackFileChecksum() const override
Verify the checksum of the entire file, returns true on success Will return true if there is no check...
void setVersion(uint32_t version)
Change the version of the VPK. Valid values are 1 and 2.
std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const override
Try to read the entry's data to a bytebuffer.
bool sign(const std::string &filename_)
Sign the VPK with the given private key KeyValues file. (See below comment).
bool removeEntry(const std::string &filename_) override
Remove an entry.
std::string getTruncatedFilestem() const override
/home/user/pak01_dir.vpk -> pak01
std::vector< MD5Entry > md5Entries
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open a VPK file.
static std::unique_ptr< PackFile > openInternal(const std::string &path, const EntryCallback &callback=nullptr)
std::vector< std::byte > signDataWithSHA256PrivateKey(std::span< const std::byte > buffer, std::span< const std::byte > privateKey)
std::array< std::byte, 16 > computeMD5(std::span< const std::byte > buffer)
bool verifySHA256PublicKey(std::span< const std::byte > buffer, std::span< const std::byte > publicKey, std::span< const std::byte > signature)
std::pair< std::string, std::string > computeSHA256KeyPair(uint16_t size=2048)
uint32_t computeCRC32(std::span< const std::byte > buffer)
std::string readFileText(const std::filesystem::path &filepath, std::size_t startOffset=0)
std::vector< std::byte > readFileBuffer(const std::filesystem::path &filepath, std::size_t startOffset=0)
std::string padNumber(int64_t number, int width)
std::vector< std::byte > decodeHex(std::string_view hex)
void normalizeSlashes(std::string &path, bool stripSlashPrefix=false, bool stripSlashSuffix=true)
bool matches(std::string_view in, std::string_view search)
A very basic regex-like pattern checker for ASCII strings.
constexpr uint32_t VPK_SIGNATURE
constexpr std::string_view VPK_DIR_SUFFIX
constexpr std::string_view VPK_KEYPAIR_PUBLIC_KEY_TEMPLATE
constexpr uint16_t VPK_ENTRY_TERM
constexpr std::string_view FPX_DIR_SUFFIX
constexpr std::string_view VPK_EXTENSION
constexpr std::string_view VPK_KEYPAIR_PRIVATE_KEY_TEMPLATE
constexpr std::string_view FPX_EXTENSION
constexpr uint16_t VPK_DIR_INDEX
constexpr uint16_t VPK_MAX_PRELOAD_BYTES
Maximum preload data size in bytes.
bool vpk_generateMD5Entries
VPK - Generate MD5 hashes for each file (VPK v2 only).
int16_t zip_compressionStrength
BSP/VPK/ZIP - Compression strength.
uint16_t vpk_preloadBytes
VPK - The amount in bytes of the file to preload. Maximum is controlled by VPK_MAX_PRELOAD_BYTES (for...
bool vpk_saveToDirectory
VPK - Save this entry to the directory VPK.