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