SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
FGP.cpp
Go to the documentation of this file.
1// ReSharper disable CppParameterMayBeConst
2// ReSharper disable CppRedundantQualifier
3
4#include <vpkpp/format/FGP.h>
5
6#include <cctype>
7#include <filesystem>
8#include <numeric>
9
10#include <BufferStream.h>
11#include <FileStream.h>
12#include <miniz.h>
13
14using namespace sourcepp;
15using namespace vpkpp;
16
17// There is only one FGP v2 in existence, and it stores absolute source file paths.
18// The game searches these absolute paths for the media subfolder. Absolutely nuts.
19// Because of that behavior though it's OK to strip the beginning of the path before bake time.
20constexpr std::string_view FGP_V2_STRIP_PATH_PREFIX = "z:/dev/valve/game/tf/";
21
22std::unique_ptr<PackFile> FGP::create(const std::string& path) {
23 {
24 FileStream stream{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
25 stream
26 .write(FGP_SIGNATURE)
27 .set_big_endian(true)
28 .write<uint32_t>(3)
29 .write<uint32_t>(0)
30 .write<uint32_t>(0);
31 }
32 return FGP::open(path);
33}
34
35std::unique_ptr<PackFile> FGP::open(const std::string& path, const EntryCallback& callback) {
36 if (!std::filesystem::exists(path)) {
37 // File does not exist
38 return nullptr;
39 }
40
41 auto* fgp = new FGP{path};
42 auto packFile = std::unique_ptr<PackFile>(fgp);
43
44 FileStream reader{fgp->fullFilePath};
45 reader.seek_in(0);
46
47 if (reader.read<uint32_t>() != FGP_SIGNATURE) {
48 // File is not an FGP
49 return nullptr;
50 }
51
52 reader.set_big_endian(true);
53
54 reader >> fgp->version;
55 if (fgp->version != 2 && fgp->version != 3) {
56 return nullptr;
57 }
58
59 const auto fileCount = reader.read<uint32_t>();
60 const auto headerSize = FGP::getHeaderSize(fgp->version, fileCount);
61
62 const auto loadingScreen = reader.read<uint32_t>();
63
64 // Here we skip to the end and load in the filename map if it exists
65 std::unordered_map<uint32_t, std::string> crackedHashes;
66 if (fgp->version == 3) {
67 const auto directoryPos = reader.tell_in();
68 if (reader.seek_in_u(sizeof(uint64_t), std::ios::end).read<uint64_t>() == FGP_SOURCEPP_FILENAMES_SIGNATURE) {
69 reader.seek_in_u(reader.seek_in(sizeof(uint64_t) * 2, std::ios::end).read<uint64_t>());
70 if (reader.read<uint64_t>() == FGP_SOURCEPP_FILENAMES_SIGNATURE && reader.read<uint32_t>() == 1) {
71 const auto filepathCount = reader.read<uint32_t>();
72 for (int i = 0; i < filepathCount; i++) {
73 const auto hash = reader.read<uint32_t>();
74 crackedHashes[hash] = fgp->cleanEntryPath(reader.read_string(reader.read<uint16_t>()));
75 }
76 }
77 }
78 reader.seek_in_u(directoryPos);
79 }
80
81 for (uint32_t i = 0; i < fileCount; i++) {
82 Entry entry = createNewEntry();
83
84 std::string entryPath;
85 if (fgp->version == 2) {
86 entryPath = fgp->cleanEntryPath(reader.read_string(260));
87 if (entryPath.starts_with(FGP_V2_STRIP_PATH_PREFIX)) {
88 entryPath = entryPath.substr(FGP_V2_STRIP_PATH_PREFIX.size());
89 }
90 } else {
91 // note: NOT a CRC32! check FGP::hashFilePath
92 entry.crc32 = reader.read<uint32_t>();
93 if (crackedHashes.contains(entry.crc32)) {
94 entryPath = crackedHashes[entry.crc32];
95 } else {
96 entryPath = fgp->cleanEntryPath(FGP_HASHED_FILEPATH_PREFIX.data() + crypto::encodeHexString({reinterpret_cast<const std::byte*>(&entry.crc32), sizeof(entry.crc32)}));
97 }
98 if (loadingScreen > 0 && i == loadingScreen) {
99 fgp->loadingScreenPath = entryPath;
100 }
101 }
102
103 entry.offset = reader.read<uint32_t>() + headerSize;
104 entry.length = reader.read<uint32_t>();
105 entry.compressedLength = reader.read<uint32_t>();
106
107 fgp->entries.emplace(entryPath, entry);
108
109 if (callback) {
110 callback(entryPath, entry);
111 }
112 }
113
114 return packFile;
115}
116
117std::optional<std::vector<std::byte>> FGP::readEntry(const std::string& path_) const {
118 const auto path = this->cleanEntryPath(path_);
119 const auto entry = this->findEntry(path);
120 if (!entry) {
121 return std::nullopt;
122 }
123 if (entry->unbaked) {
124 return PackFile::readUnbakedEntry(*entry);
125 }
126
127 // It's baked into the file on disk
128 FileStream stream{this->fullFilePath};
129 stream.seek_in_u(entry->offset);
130 if (entry->compressedLength == 0) {
131 return stream.read_bytes(entry->length);
132 }
133
134 // Decode
135 const auto compressedData = stream.read_bytes(entry->compressedLength);
136 std::vector<std::byte> data(entry->length);
137 mz_ulong len = entry->length;
138 if (mz_uncompress(reinterpret_cast<unsigned char*>(data.data()), &len, reinterpret_cast<const unsigned char*>(compressedData.data()), entry->compressedLength) != MZ_OK) {
139 return std::nullopt;
140 }
141 return data;
142}
143
144bool FGP::renameEntry(const std::string& oldPath, const std::string& newPath) {
145 if (PackFile::renameEntry(oldPath, newPath)) {
146 if (this->loadingScreenPath == oldPath) {
147 this->loadingScreenPath = newPath;
148 }
149 return true;
150 }
151 return false;
152}
153
154bool FGP::renameDirectory(const std::string& oldDir, const std::string& newDir) {
155 if (PackFile::renameDirectory(oldDir, newDir)) {
156 if (const auto cleanOldDir = this->cleanEntryPath(oldDir) + '/'; this->loadingScreenPath.starts_with(cleanOldDir)) {
157 this->loadingScreenPath = this->cleanEntryPath(newDir) + '/' + this->loadingScreenPath.substr(cleanOldDir.size());
158 }
159 return true;
160 }
161 return false;
162}
163
164bool FGP::removeEntry(const std::string& path) {
165 if (PackFile::removeEntry(path)) {
166 if (this->loadingScreenPath == path) {
167 this->loadingScreenPath = "";
168 }
169 return true;
170 }
171 return false;
172}
173
174std::size_t FGP::removeDirectory(const std::string& dirName) {
175 if (PackFile::removeDirectory(dirName)) {
176 if (this->loadingScreenPath.starts_with(this->cleanEntryPath(dirName) + '/')) {
177 this->loadingScreenPath = "";
178 }
179 return true;
180 }
181 return false;
182}
183
184void FGP::addEntryInternal(Entry& entry, const std::string& path, std::vector<std::byte>& buffer, EntryOptions options) {
185 // note: NOT a CRC32! check FGP::hashFilePath
186 entry.crc32 = FGP::hashFilePath(this->cleanEntryPath(path));
187 entry.length = buffer.size();
188 entry.compressedLength = 0;
189
190 // Offset will be reset when it's baked
191 entry.offset = 0;
192}
193
194bool FGP::bake(const std::string& outputDir_, BakeOptions options, const EntryCallback& callback) {
195 // Get the proper file output folder
196 const std::string outputDir = this->getBakeOutputDir(outputDir_);
197 const std::string outputPath = outputDir + '/' + this->getFilename();
198
199 // Reconstruct data for ease of access
200 std::vector<std::pair<std::string, Entry*>> entriesToBake;
201 this->runForAllEntriesInternal([&entriesToBake](const std::string& path, Entry& entry) {
202 entriesToBake.emplace_back(path, &entry);
203 });
204 const auto headerSize = FGP::getHeaderSize(this->version, entriesToBake.size());
205
206 // Read data before overwriting, we don't know if we're writing to ourself
207 std::vector<std::byte> fileData;
208 {
209 FileStream stream{this->fullFilePath};
210 for (const auto& [path, entry] : entriesToBake) {
211 if (!entry->unbaked) {
212 stream.seek_in_u(entry->offset);
213 const auto binData = stream.read_bytes(entry->compressedLength > 0 ? entry->compressedLength : entry->length);
214 entry->offset = headerSize + fileData.size();
215 fileData.insert(fileData.end(), binData.begin(), binData.end());
216 } else if (const auto binData = this->readEntry(path)) {
217 entry->offset = headerSize + fileData.size();
218 entry->length = binData->size();
219 entry->compressedLength = 0;
220
221 if (!options.zip_compressionStrength) {
222 fileData.insert(fileData.end(), binData->begin(), binData->end());
223 } else {
224 mz_ulong compressedSize = mz_compressBound(binData->size());
225 std::vector<std::byte> out(compressedSize);
226
227 int status = MZ_OK;
228 while ((status = mz_compress2(reinterpret_cast<unsigned char*>(out.data()), &compressedSize, reinterpret_cast<const unsigned char*>(binData->data()), binData->size(), options.zip_compressionStrength)) == MZ_BUF_ERROR) {
229 compressedSize *= 2;
230 out.resize(compressedSize);
231 }
232
233 if (status != MZ_OK) {
234 fileData.insert(fileData.end(), binData->begin(), binData->end());
235 continue;
236 }
237 out.resize(compressedSize);
238 fileData.insert(fileData.end(), out.begin(), out.end());
239 entry->compressedLength = compressedSize;
240 }
241 } else {
242 entry->offset = 0;
243 entry->length = 0;
244 entry->compressedLength = 0;
245 }
246 }
247 }
248
249 {
250 FileStream stream{outputPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
251 stream.seek_out(0);
252
253 stream << FGP_SIGNATURE;
254 stream.set_big_endian(true);
255
256 stream
257 .write<uint32_t>(this->version)
258 .write<uint32_t>(entriesToBake.size());
259
260 const auto loadingScreenPos = stream.tell_out();
261 stream.write<uint32_t>(0);
262
263 // Directory
264 uint32_t i = 0;
265 for (const auto& [path, entry] : entriesToBake) {
266 if (path == this->loadingScreenPath) {
267 const auto curPos = stream.tell_out();
268 stream.seek_out_u(loadingScreenPos).write<uint32_t>(i).seek_out_u(curPos);
269 }
270
271 if (this->version == 2) {
272 stream.write(path, false, 260);
273 } else {
274 stream.write<uint32_t>(entry->crc32);
275 }
276
277 stream
278 .write<uint32_t>(entry->offset - headerSize)
279 .write<uint32_t>(entry->length)
280 .write<uint32_t>(entry->compressedLength);
281
282 if (callback) {
283 callback(path, *entry);
284 }
285
286 i++;
287 }
288
289 // File data
290 stream.write(fileData);
291
292 // Filename mappings
293 if (this->version == 3) {
294 const auto filenameMappingPos = stream.tell_out();
295 stream
297 .write<uint32_t>(1); // version
298 const auto filepathCountPos = stream.tell_out();
299 stream.write<uint32_t>(0);
300 uint32_t filepathCount = 0;
301 for (const auto& [path, entry] : entriesToBake) {
302 if (path.starts_with(FGP_HASHED_FILEPATH_PREFIX)) {
303 continue;
304 }
305 stream
306 .write<uint32_t>(entry->crc32)
307 .write<uint16_t>(path.size())
308 .write(path, false);
309 filepathCount++;
310 }
311 stream
312 .write<uint64_t>(filenameMappingPos)
314 .seek_out_u(filepathCountPos)
315 .write<uint32_t>(filepathCount);
316 }
317 }
318
319 // Clean up
320 this->mergeUnbakedEntries();
321 PackFile::setFullFilePath(outputDir);
322 return true;
323}
324
326 using enum Attribute;
327 return LENGTH;
328}
329
330FGP::operator std::string() const {
331 return PackFile::operator std::string() +
332 " | Version v" + std::to_string(this->version);
333}
334
335std::string FGP::getLoadingScreenFilePath() const {
336 return this->loadingScreenPath;
337}
338
339void FGP::setLoadingScreenFilePath(const std::string& path) {
340 if (this->hasEntry(path)) {
341 this->loadingScreenPath = this->cleanEntryPath(path);
342 }
343}
344
345uint32_t FGP::hashFilePath(const std::string& filepath) {
346 return std::accumulate(filepath.begin(), filepath.end(), 0xAAAAAAAAu, [](uint32_t hash, char c) { return (hash << 5) + hash + static_cast<uint8_t>(tolower(c)); });
347}
348
349uint32_t FGP::getHeaderSize(uint32_t version, uint32_t fileCount) {
350 return ((version == 2 ? 260 : sizeof(uint32_t)) + sizeof(uint32_t) * 3) * fileCount + sizeof(uint32_t) * 4;
351}
constexpr std::string_view FGP_V2_STRIP_PATH_PREFIX
Definition FGP.cpp:20
This class represents the metadata that a file has inside a PackFile.
Definition Entry.h:14
bool unbaked
Used to check if entry is saved to disk.
Definition Entry.h:43
uint64_t offset
Offset, format-specific meaning - 0 if unused, or if the offset genuinely is 0.
Definition Entry.h:33
uint64_t compressedLength
If the format supports compression, this is the compressed length.
Definition Entry.h:30
uint32_t crc32
CRC32 checksum - 0 if unused.
Definition Entry.h:40
uint64_t length
Length in bytes (in formats with compression, this is the uncompressed length)
Definition Entry.h:26
void addEntryInternal(Entry &entry, const std::string &path, std::vector< std::byte > &buffer, EntryOptions options) override
Definition FGP.cpp:184
void setLoadingScreenFilePath(const std::string &path)
Definition FGP.cpp:339
static std::unique_ptr< PackFile > create(const std::string &path)
Create an FGP file.
Definition FGP.cpp:22
std::string loadingScreenPath
Definition FGP.h:61
bool renameEntry(const std::string &oldPath, const std::string &newPath) override
Rename an existing entry.
Definition FGP.cpp:144
std::optional< std::vector< std::byte > > readEntry(const std::string &path_) const override
Try to read the entry's data to a bytebuffer.
Definition FGP.cpp:117
uint32_t version
Definition FGP.h:60
bool renameDirectory(const std::string &oldDir, const std::string &newDir) override
Rename an existing directory.
Definition FGP.cpp:154
std::size_t removeDirectory(const std::string &dirName) override
Remove a directory.
Definition FGP.cpp:174
std::string getLoadingScreenFilePath() const
Definition FGP.cpp:335
static uint32_t hashFilePath(const std::string &filepath)
Definition FGP.cpp:345
Attribute getSupportedEntryAttributes() const override
Returns a list of supported entry attributes Mostly for GUI programs that show entries and their meta...
Definition FGP.cpp:325
bool removeEntry(const std::string &path) override
Remove an entry.
Definition FGP.cpp:164
static std::unique_ptr< PackFile > open(const std::string &path, const EntryCallback &callback=nullptr)
Open an FGP file.
Definition FGP.cpp:35
static uint32_t getHeaderSize(uint32_t version, uint32_t fileCount)
Definition FGP.cpp:349
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 FGP.cpp:194
EntryCallbackBase< void > EntryCallback
Definition PackFile.h:38
virtual std::size_t removeDirectory(const std::string &dirName_)
Remove a directory.
Definition PackFile.cpp:336
virtual bool renameDirectory(const std::string &oldDir_, const std::string &newDir_)
Rename an existing directory.
Definition PackFile.cpp:290
void mergeUnbakedEntries()
Definition PackFile.cpp:658
std::optional< Entry > findEntry(const std::string &path_, bool includeUnbaked=true) const
Try to find an entry given the file path.
Definition PackFile.cpp:166
std::string fullFilePath
Definition PackFile.h:232
void runForAllEntriesInternal(const std::function< void(const std::string &, Entry &)> &operation, bool includeUnbaked=true)
Definition PackFile.cpp:544
bool hasEntry(const std::string &path, bool includeUnbaked=true) const
Check if an entry exists given the file path.
Definition PackFile.cpp:162
virtual bool renameEntry(const std::string &oldPath_, const std::string &newPath_)
Rename an existing entry.
Definition PackFile.cpp:270
std::string getFilename() const
/home/user/pak01_dir.vpk -> pak01_dir.vpk
Definition PackFile.cpp:594
std::string getBakeOutputDir(const std::string &outputDir) const
Definition PackFile.cpp:643
void setFullFilePath(const std::string &outputDir)
Definition PackFile.cpp:674
std::string cleanEntryPath(const std::string &path) const
Definition PackFile.cpp:679
static Entry createNewEntry()
Definition PackFile.cpp:688
virtual bool removeEntry(const std::string &path_)
Remove an entry.
Definition PackFile.cpp:319
static std::optional< std::vector< std::byte > > readUnbakedEntry(const Entry &entry)
Definition PackFile.cpp:692
std::string encodeHexString(std::span< const std::byte > hex)
Definition String.cpp:21
constexpr std::string_view FGP_HASHED_FILEPATH_PREFIX
Definition FGP.h:14
Attribute
Definition Attribute.h:7
constexpr auto FGP_SOURCEPP_FILENAMES_SIGNATURE
Definition FGP.h:15
constexpr auto FGP_SIGNATURE
Definition FGP.h:11
int16_t zip_compressionStrength
BSP/VPK/ZIP - Compression strength.
Definition Options.h:24