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