SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
VPK_VTMB.cpp
Go to the documentation of this file.
1// ReSharper disable CppRedundantQualifier
2
4
5#include <filesystem>
6
7#include <FileStream.h>
9#include <sourcepp/String.h>
10
11using namespace sourcepp;
12using namespace vpkpp;
13
14std::unique_ptr<PackFile> VPK_VTMB::create(const std::string& path) {
15 {
16 FileStream stream{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
17 stream
18 .write<uint32_t>(0)
19 .write<uint32_t>(0)
20 .write<uint8_t>(0);
21 }
22 return VPK_VTMB::open(path);
23}
24
25std::unique_ptr<PackFile> VPK_VTMB::open(const std::string& path, const EntryCallback& callback) {
26 if (!std::filesystem::exists(path)) {
27 // File does not exist
28 return nullptr;
29 }
30
31 // Extra check to make sure this is a VTMB VPK path
32 const auto stem = std::filesystem::path{path}.stem().string();
33 if (stem.length() != 7 || !stem.starts_with("pack") || !parser::text::isNumber(stem.substr(4))) {
34 return nullptr;
35 }
36
37 auto* vpkVTMB = new VPK_VTMB{path};
38 auto packFile = std::unique_ptr<PackFile>(vpkVTMB);
39
40 for (int i = 0; i <= 9; i++) {
41 if (!std::filesystem::exists(vpkVTMB->getTruncatedFilepath() + string::padNumber(i * 100, 3) + VPK_VTMB_EXTENSION.data())) {
42 break;
43 }
44 for (int j = 0; j <= 99; j++) {
45 auto numberedPath = vpkVTMB->getTruncatedFilepath() + string::padNumber(i * 100 + j, 3) + VPK_VTMB_EXTENSION.data();
46 if (!std::filesystem::exists(numberedPath)) {
47 break;
48 }
49 vpkVTMB->openNumbered(i * 100 + j, numberedPath, callback);
50 }
51 }
52
53 if (vpkVTMB->knownArchives.empty()) {
54 uint32_t archiveIndex;
55 string::toInt(stem.substr(4), archiveIndex);
56 vpkVTMB->openNumbered(archiveIndex, path, callback);
57 }
58
59 vpkVTMB->currentArchive++;
60 return packFile;
61}
62
63void VPK_VTMB::openNumbered(uint32_t archiveIndex, const std::string& path, const EntryCallback& callback) {
64 FileStream reader{path};
65 reader.seek_in(sizeof(uint32_t) * 2 + sizeof(uint8_t), std::ios::end);
66
67 const auto fileCount = reader.read<uint32_t>();
68 const auto dirOffset = reader.read<uint32_t>();
69
70 // Make 100% sure
71 const auto version = reader.read<uint8_t>();
72 if (version != 0) {
73 return;
74 }
75
76 // Ok now let's load this thing
77 this->knownArchives.push_back(archiveIndex);
78 if (archiveIndex > this->currentArchive) {
79 this->currentArchive = archiveIndex;
80 }
81
82 reader.seek_in(dirOffset);
83 for (uint32_t i = 0; i < fileCount; i++) {
84 Entry entry = createNewEntry();
85 entry.archiveIndex = archiveIndex;
86
87 auto entryPath = this->cleanEntryPath(reader.read_string(reader.read<uint32_t>()));
88
89 entry.offset = reader.read<uint32_t>();
90 entry.length = reader.read<uint32_t>();
91
92 this->entries.emplace(entryPath, entry);
93
94 if (callback) {
95 callback(entryPath, entry);
96 }
97 }
98}
99
100std::optional<std::vector<std::byte>> VPK_VTMB::readEntry(const std::string& path_) const {
101 const auto path = this->cleanEntryPath(path_);
102 const auto entry = this->findEntry(path);
103 if (!entry) {
104 return std::nullopt;
105 }
106 if (entry->unbaked) {
107 return readUnbakedEntry(*entry);
108 }
109
110 // It's baked into the file on disk
111 FileStream stream{this->getTruncatedFilepath() + string::padNumber(entry->archiveIndex, 3) + VPK_VTMB_EXTENSION.data()};
112 if (!stream) {
113 return std::nullopt;
114 }
115 stream.seek_in_u(entry->offset);
116 return stream.read_bytes(entry->length);
117}
118
119void VPK_VTMB::addEntryInternal(Entry& entry, const std::string& path, std::vector<std::byte>& buffer, EntryOptions options) {
120 entry.archiveIndex = this->currentArchive;
121 entry.length = buffer.size();
122
123 // Offset will be reset when it's baked
124 entry.offset = 0;
125}
126
127bool VPK_VTMB::bake(const std::string& outputDir_, BakeOptions options, const EntryCallback& callback) {
128 if (this->knownArchives.empty()) {
129 return false;
130 }
131
132 // Get the proper file output folder
133 const std::string outputDir = this->getBakeOutputDir(outputDir_);
134 const std::string outputPathStem = outputDir + '/' + this->getTruncatedFilestem();
135
136 // Copy files to temp dir and change current path
137 const auto tempDir = std::filesystem::temp_directory_path() / string::generateUUIDv4();
138 std::error_code ec;
139 if (!std::filesystem::create_directory(tempDir, ec)) {
140 return false;
141 }
142 ec.clear();
143 for (const auto vpkIndex : this->knownArchives) {
144 std::filesystem::copy(outputPathStem + string::padNumber(vpkIndex, 3) + VPK_VTMB_EXTENSION.data(), tempDir, ec);
145 if (ec) {
146 return false;
147 }
148 ec.clear();
149 }
150 this->fullFilePath = (tempDir / this->getTruncatedFilestem()).string() + string::padNumber(this->knownArchives[0], 3) + VPK_VTMB_EXTENSION.data();
151
152 // Reconstruct data for ease of access
153 std::unordered_map<uint16_t, std::vector<std::pair<std::string, Entry*>>> entriesToBake;
154 this->runForAllEntriesInternal([&entriesToBake](const std::string& path, Entry& entry) {
155 if (!entriesToBake.contains(entry.archiveIndex)) {
156 entriesToBake[entry.archiveIndex] = {};
157 }
158 entriesToBake[entry.archiveIndex].emplace_back(path, &entry);
159 });
160
161 for (auto& [archiveIndex, entriesToBakeInArchive] : entriesToBake) {
162 FileStream stream{outputPathStem + string::padNumber(archiveIndex, 3) + VPK_VTMB_EXTENSION.data(), FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
163 stream.seek_out(0);
164
165 // File data
166 for (auto& [path, entry] : entriesToBakeInArchive) {
167 if (auto binData = this->readEntry(path)) {
168 entry->offset = stream.tell_out();
169 stream.write(*binData);
170 } else {
171 entry->offset = 0;
172 entry->length = 0;
173 }
174 }
175
176 // Directory
177 const auto dirOffset = stream.tell_out();
178 for (const auto& [path, entry] : entriesToBakeInArchive) {
179 stream.write<uint32_t>(path.length());
180 stream.write(path, false);
181 stream.write<uint32_t>(entry->offset);
182 stream.write<uint32_t>(entry->length);
183
184 if (callback) {
185 callback(path, *entry);
186 }
187 }
188
189 // Footer
190 stream.write<uint32_t>(entriesToBakeInArchive.size());
191 stream.write<uint32_t>(dirOffset);
192 stream.write<uint8_t>(0);
193 }
194
195 if (entriesToBake.contains(this->currentArchive)) {
196 this->currentArchive++;
197 }
198
199 // Clean up
200 this->mergeUnbakedEntries();
201 std::filesystem::remove_all(tempDir, ec);
202 PackFile::setFullFilePath(outputDir);
203 return true;
204}
205
207 // Ok so technically these things can have names besides packXXX, but the game won't recognize it...
208 // and I check the filename starts with "pack" in VPK_VTMB::open, so I'm allowed to do this :P
209 return "pack";
210}
211
This class represents the metadata that a file has inside a PackFile.
Definition Entry.h:14
uint64_t offset
Offset, format-specific meaning - 0 if unused, or if the offset genuinely is 0.
Definition Entry.h:33
uint32_t archiveIndex
Which external archive this entry is in.
Definition Entry.h:23
uint64_t length
Length in bytes (in formats with compression, this is the uncompressed length).
Definition Entry.h:26
EntryCallbackBase< void > EntryCallback
Definition PackFile.h:38
void mergeUnbakedEntries()
Definition PackFile.cpp:685
std::optional< Entry > findEntry(const std::string &path_, bool includeUnbaked=true) const
Try to find an entry given the file path.
Definition PackFile.cpp:172
std::string fullFilePath
Definition PackFile.h:231
EntryTrie entries
Definition PackFile.h:232
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
Definition PackFile.cpp:571
bool bake()
If output folder is an empty string, it will overwrite the original.
Definition PackFile.cpp:369
std::string getBakeOutputDir(const std::string &outputDir) const
Definition PackFile.cpp:670
std::string getTruncatedFilepath() const
/home/user/pak01_dir.vpk -> /home/user/pak01
Definition PackFile.cpp:617
void setFullFilePath(const std::string &outputDir)
Definition PackFile.cpp:701
std::string cleanEntryPath(const std::string &path) const
Definition PackFile.cpp:706
static Entry createNewEntry()
Definition PackFile.cpp:715
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Definition PackFile.cpp:719
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open Vampire: The Masquerade - Bloodlines VPK files.
Definition VPK_VTMB.cpp:25
static std::unique_ptr< PackFile > create(const std::string &path)
Create Vampire: The Masquerade - Bloodlines VPK files.
Definition VPK_VTMB.cpp:14
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
Definition VPK_VTMB.cpp:212
std::vector< uint32_t > knownArchives
Definition VPK_VTMB.h:34
std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const override
Try to read the entry's data to a bytebuffer.
Definition VPK_VTMB.cpp:100
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
Definition VPK_VTMB.cpp:119
std::string getTruncatedFilestem() const override
/home/user/pak01_dir.vpk -> pak01
Definition VPK_VTMB.cpp:206
void openNumbered(uint32_t archiveIndex, const std::string &path, const EntryCallback &callback)
Definition VPK_VTMB.cpp:63
uint32_t currentArchive
Definition VPK_VTMB.h:35
bool isNumber(char c)
If a char is a numerical character (0-9).
Definition Text.cpp:48
std::string padNumber(int64_t number, int width)
Definition String.cpp:222
std::from_chars_result toInt(std::string_view number, std::integral auto &out, int base=10)
Definition String.h:81
std::string generateUUIDv4()
Definition String.cpp:198
constexpr std::string_view VPK_VTMB_EXTENSION
Definition VPK_VTMB.h:9
Attribute
Definition Attribute.h:7