SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
BSP.cpp
Go to the documentation of this file.
1// ReSharper disable CppRedundantParentheses
2
3#include <bsppp/BSP.h>
4
5#include <algorithm>
6#include <filesystem>
7#include <type_traits>
8#include <utility>
9
10#include <FileStream.h>
12
13using namespace bsppp;
14using namespace sourcepp;
15
16namespace {
17
18template<BufferStreamPODType T>
19[[nodiscard]] std::vector<T> parseLumpContents(const BSP& bsp, BSPLump lump, void(*callback)(const BSP&, BufferStreamReadOnly&, std::vector<T>&) = [](const BSP&, BufferStreamReadOnly& stream, std::vector<T>& out) {
20 stream.read(out, stream.size() / sizeof(T));
21}) {
22 auto data = bsp.getLumpData(lump);
23 if (!data) {
24 return {};
25 }
26
27 BufferStreamReadOnly stream{*data};
28 stream.set_big_endian(bsp.isConsole());
29 std::vector<T> out;
30 callback(bsp, stream, out);
31 return out;
32}
33
34template<BufferStreamPODType Old, BufferStreamPODType New>
35requires requires(New) { {New::upgrade(Old{})} -> std::same_as<New>; }
36void parseAndUpgrade(BufferStreamReadOnly& stream, std::vector<New>& out) {
37 std::vector<Old> old;
38 stream.read(old, stream.size() / sizeof(Old));
39 for (const auto& elem : old) {
40 out.push_back(New::upgrade(elem));
41 }
42}
43
44} // namespace
45
46BSP::BSP(std::string path_, bool loadPatchFiles)
47 : path(std::move(path_)) {
48 if (!this->readHeader()) {
49 this->path.clear();
50 return;
51 }
52
53 this->stagedVersion = this->header.version;
54 this->stagedGameLumps = this->parseGameLumps(false);
55 this->stagedMapRevision = this->header.mapRevision;
56
57 if (loadPatchFiles) {
58 const auto fsPath = std::filesystem::path{this->path};
59 const auto fsStem = (fsPath.parent_path() / fsPath.stem()).string() + "_l_";
60
61 for (int i = 0; ; i++) {
62 auto patchFilePath = fsStem + std::to_string(i) + ".lmp";
63 if (!std::filesystem::exists(patchFilePath)) {
64 break;
65 }
66 this->setLumpFromPatchFile(patchFilePath);
67 }
68 }
69}
70
71BSP::operator bool() const {
72 return !this->path.empty();
73}
74
75BSP BSP::create(std::string path, uint32_t version, uint32_t mapRevision) {
76 {
77 FileStream writer{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
78 writer << BSP_SIGNATURE << version;
79 if (version == 27) {
80 writer.write<uint32_t>(0);
81 }
82 for (int i = 0; i < BSP_LUMP_COUNT; i++) {
83 writer
84 .write<uint32_t>(0)
85 .write<uint32_t>(0)
86 .write<uint32_t>(0)
87 .write<uint32_t>(0);
88 }
89 writer << mapRevision;
90 }
91 return BSP{std::move(path)};
92}
93
94uint32_t BSP::getVersion() const {
95 return this->stagedVersion;
96}
97
98void BSP::setVersion(uint32_t version) {
99 this->stagedVersion = version;
100}
101
102uint32_t BSP::getMapRevision() const {
103 return this->stagedMapRevision;
104}
105
106void BSP::setMapRevision(uint32_t mapRevision) {
107 this->stagedMapRevision = mapRevision;
108}
109
110bool BSP::isL4D2() const {
111 return this->l4d2;
112}
113
115 this->l4d2 = isL4D2;
116}
117
118bool BSP::isConsole() const {
119 return this->console;
120}
121
123 this->console = isConsole;
124}
125
126bool BSP::hasLump(BSPLump lumpIndex) const {
127 if (this->path.empty()) {
128 return false;
129 }
130 const auto lump = static_cast<std::underlying_type_t<BSPLump>>(lumpIndex);
131 return (this->stagedLumps.contains(lump) && this->stagedLumps.at(lump).first.length != 0 && this->stagedLumps.at(lump).first.offset != 0)
132 || (this->header.lumps[lump].length != 0 && this->header.lumps[lump].offset != 0);
133}
134
135bool BSP::isLumpCompressed(BSPLump lumpIndex) const {
136 if (this->hasLump(lumpIndex)) {
137 const auto lump = static_cast<std::underlying_type_t<BSPLump>>(lumpIndex);
138 return (this->stagedLumps.contains(lump) && this->stagedLumps.at(lump).first.uncompressedLength > 0) || this->header.lumps[lump].uncompressedLength > 0;
139 }
140 return false;
141}
142
143uint32_t BSP::getLumpVersion(BSPLump lumpIndex) const {
144 if (this->path.empty()) {
145 return 0;
146 }
147 const auto lump = static_cast<std::underlying_type_t<BSPLump>>(lumpIndex);
148 if (this->stagedLumps.contains(lump)) {
149 return this->stagedLumps.at(lump).first.version;
150 }
151 return this->header.lumps[lump].version;
152}
153
154std::optional<std::vector<std::byte>> BSP::getLumpData(BSPLump lumpIndex, bool noDecompression) const {
155 if (this->path.empty() || !this->hasLump(lumpIndex)) {
156 return std::nullopt;
157 }
158
159 const auto lump = static_cast<std::underlying_type_t<BSPLump>>(lumpIndex);
160 std::vector<std::byte> lumpBytes;
161
162 if (this->stagedLumps.contains(lump)) {
163 lumpBytes = this->stagedLumps.at(lump).second;
164 } else {
165 FileStream reader{this->path};
166 lumpBytes = reader
167 .seek_in(this->header.lumps[lump].offset)
168 .read_bytes(this->header.lumps[lump].length);
169 }
170
171 if (!noDecompression && this->isLumpCompressed(lumpIndex)) {
172 return compression::decompressValveLZMA(lumpBytes);
173 }
174 return lumpBytes;
175}
176
177bool BSP::setLump(BSPLump lumpIndex, uint32_t version, std::span<const std::byte> data, uint8_t compressLevel) {
178 if (this->path.empty() || lumpIndex == BSPLump::UNKNOWN) {
179 return false;
180 }
181 if (lumpIndex == BSPLump::GAME_LUMP) {
182 // Game lump needs to be modified with other methods
183 return false;
184 }
185
186 if (lumpIndex == BSPLump::PAKFILE) {
187 // Paklump should use zip compression
188 compressLevel = 0;
189 }
190
191 const auto lump = static_cast<std::underlying_type_t<BSPLump>>(lumpIndex);
192 if (compressLevel > 0) {
193 auto compressedData = compression::compressValveLZMA(data, compressLevel);
194 if (!compressedData) {
195 return false;
196 }
197 this->stagedLumps[lump] = std::make_pair<Lump, std::vector<std::byte>>({
198 .version = version, .uncompressedLength = static_cast<uint32_t>(data.size()),
199 }, {compressedData->begin(), compressedData->end()});
200 } else {
201 this->stagedLumps[lump] = std::make_pair<Lump, std::vector<std::byte>>({
202 .version = version, .uncompressedLength = 0,
203 }, {data.begin(), data.end()});
204 }
205 return true;
206}
207
208bool BSP::setLump(uint32_t version, std::span<const BSPEntityKeyValues> data, uint8_t compressLevel) {
209 if (version > 1) {
210 return false;
211 }
212 std::string out;
213 for (const auto& ent : data) {
214 out += ent.bake(version == 1) + '\n';
215 }
216 if (out.empty()) {
217 out += '\0';
218 } else {
219 out.back() = '\0';
220 }
221 return this->setLump(BSPLump::ENTITIES, version, {reinterpret_cast<const std::byte*>(out.data()), out.size()}, compressLevel);
222}
223
225 for (const auto& gameLump : this->stagedGameLumps) {
226 if (gameLump.signature == signature) {
227 return gameLump.isCompressed;
228 }
229 }
230 return false;
231}
232
234 for (const auto& gameLump : this->stagedGameLumps) {
235 if (gameLump.signature == signature) {
236 return gameLump.version;
237 }
238 }
239 return 0;
240}
241
242std::optional<std::vector<std::byte>> BSP::getGameLumpData(BSPGameLump::Signature signature) const {
243 for (const auto& gameLump : this->stagedGameLumps) {
244 if (gameLump.signature == signature) {
245 if (gameLump.isCompressed) {
246 return compression::decompressValveLZMA(gameLump.data);
247 }
248 return gameLump.data;
249 }
250 }
251 return std::nullopt;
252}
253
254bool BSP::setGameLump(BSPGameLump::Signature signature, uint16_t version, std::span<const std::byte> data, uint8_t compressLevel) {
255 BSPGameLump gameLump{
256 .signature = signature,
257 .isCompressed = compressLevel > 0,
258 .version = version,
259 .offset = 0,
260 .uncompressedLength = static_cast<uint32_t>(data.size()),
261 };
262
263 if (compressLevel) {
264 const auto compressedData = compression::compressValveLZMA(data, compressLevel);
265 if (!compressedData) {
266 return false;
267 }
268 gameLump.data = *compressedData;
269 } else {
270 gameLump.data = {data.begin(), data.end()};
271 }
272
273 this->stagedGameLumps.push_back(gameLump);
274 return true;
275}
276
277void BSP::resetLump(BSPLump lumpIndex) {
278 if (this->path.empty()) {
279 return;
280 }
281 if (lumpIndex == BSPLump::UNKNOWN) {
282 this->reset();
283 return;
284 }
285 if (lumpIndex == BSPLump::GAME_LUMP) {
286 this->stagedGameLumps = this->parseGameLumps(false);
287 return;
288 }
289 const auto lump = static_cast<std::underlying_type_t<BSPLump>>(lumpIndex);
290 if (this->stagedLumps.contains(lump)) {
291 this->stagedLumps.erase(lump);
292 }
293}
294
296 if (this->path.empty()) {
297 return;
298 }
299 this->stagedVersion = this->header.version;
300 this->stagedLumps.clear();
301 this->stagedGameLumps = this->parseGameLumps(false);
302 this->stagedMapRevision = this->header.mapRevision;
303}
304
305void BSP::createLumpPatchFile(BSPLump lumpIndex) const {
306 if (this->path.empty()) {
307 return;
308 }
309
310 const auto lumpData = this->getLumpData(lumpIndex);
311 if (!lumpData) {
312 return;
313 }
314
315 const auto& [
316 lumpOffset,
317 lumpLength,
318 lumpVersion,
319 lumpUncompressedLength
320 ] = this->header.lumps.at(static_cast<std::underlying_type_t<BSPLump>>(lumpIndex));
321
322 const auto fsPath = std::filesystem::path{this->path};
323 const auto fsStem = (fsPath.parent_path() / fsPath.stem()).string() + "_l_";
324 int nonexistentNumber = 0;
325 while (true) {
326 if (!std::filesystem::exists(fsStem + std::to_string(nonexistentNumber) + ".lmp")) {
327 break;
328 }
329 nonexistentNumber++;
330 }
331
332 FileStream writer{fsStem + std::to_string(nonexistentNumber) + ".lmp", FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
333 writer
334 .seek_out(0)
335 .write<int32_t>(sizeof(int32_t) * 5)
336 .write(lumpIndex)
337 .write(lumpVersion)
338 .write(lumpLength)
339 .write(this->header.mapRevision)
340 .write(*lumpData);
341}
342
343bool BSP::setLumpFromPatchFile(const std::string& lumpFilePath) {
344 if (this->path.empty()) {
345 return false;
346 }
347
348 FileStream reader{lumpFilePath};
349 if (!reader) {
350 return false;
351 }
352
353 const auto offset = reader.read<uint32_t>();
354 const auto index = reader.read<uint32_t>();
355 const auto version = reader.read<uint32_t>();
356 const auto length = reader.read<uint32_t>();
357 if (index > BSP_LUMP_COUNT || offset == 0 || length == 0) {
358 return false;
359 }
360
361 this->setLump(static_cast<BSPLump>(index), version, reader.seek_in(offset).read_bytes(length));
362 return true;
363}
364
365bool BSP::bake(std::string_view outputPath) {
366 if (this->path.empty()) {
367 return false;
368 }
369
370 std::vector<std::byte> out;
371 BufferStream stream{out};
372 stream.set_big_endian(this->console);
373
374 stream << BSP_SIGNATURE << this->stagedVersion;
375 if (this->stagedVersion == 27) {
376 // Contagion funny
377 stream.write<uint32_t>(0);
378 }
379
380 const auto lumpsHeaderOffset = stream.tell();
381 for (int i = 0; i < sizeof(Header::lumps); i++) {
382 stream.write<uint8_t>(0);
383 }
384
385 stream << this->stagedMapRevision;
386
387 for (auto i : BSP_LUMP_ORDER) {
388 if (!this->hasLump(static_cast<BSPLump>(i))) {
389 const auto curPos = stream.tell();
390 stream.seek_u(lumpsHeaderOffset + i * sizeof(Lump));
391
392 const auto& lump = this->header.lumps[i];
393 if (!this->l4d2) {
394 stream
395 .write<uint32_t>(0)
396 .write<uint32_t>(0)
397 .write<uint32_t>(lump.version);
398 } else {
399 stream
400 .write<uint32_t>(lump.version)
401 .write<uint32_t>(0)
402 .write<uint32_t>(0);
403 }
404 stream
405 .write<uint32_t>(0)
406 .seek_u(curPos);
407 continue;
408 }
409
410 // Lumps are 4 byte aligned
411 if (const auto padding = math::paddingForAlignment(4, stream.tell()); padding > 0) {
412 stream.pad(padding);
413 }
414
415 if (static_cast<BSPLump>(i) == BSPLump::GAME_LUMP && !this->stagedGameLumps.empty()) {
416 const auto gameLumpOffset = stream.tell();
417
418 bool oneOrMoreGameLumpCompressed = false;
419 for (const auto& gameLump : this->stagedGameLumps) {
420 if (gameLump.isCompressed) {
421 oneOrMoreGameLumpCompressed = true;
422 break;
423 }
424 }
425 // NOLINTNEXTLINE(*-sizeof-container)
426 auto gameLumpCurrentOffset = !this->console * stream.tell() + sizeof(int32_t) + ((sizeof(BSPGameLump) - sizeof(BSPGameLump::data)) * (this->stagedGameLumps.size() + oneOrMoreGameLumpCompressed));
427 stream.write<uint32_t>(this->stagedGameLumps.size() + oneOrMoreGameLumpCompressed);
428
429 for (const auto& gameLump : this->stagedGameLumps) {
430 if (gameLump.signature == 0) {
431 break;
432 }
433 stream
434 .write<uint32_t>(gameLump.signature)
435 .write<uint16_t>(gameLump.isCompressed)
436 .write<uint16_t>(gameLump.version)
437 .write<uint32_t>(gameLumpCurrentOffset)
438 .write<uint32_t>(gameLump.uncompressedLength);
439 gameLumpCurrentOffset += gameLump.data.size();
440 }
441 if (oneOrMoreGameLumpCompressed) {
442 stream
443 .write<uint32_t>(0)
444 .write<uint16_t>(0)
445 .write<uint16_t>(0)
446 .write<uint32_t>(gameLumpCurrentOffset)
447 .write<uint32_t>(0);
448 }
449
450 for (const auto& gameLump : this->stagedGameLumps) {
451 if (gameLump.signature == 0) {
452 break;
453 }
454 stream.write(gameLump.data);
455 }
456
457 const auto curPos = stream.tell();
458 stream.seek_u(lumpsHeaderOffset + i * sizeof(Lump));
459 if (!this->l4d2) {
460 stream
461 .write<uint32_t>(gameLumpOffset)
462 .write<uint32_t>(curPos - gameLumpOffset)
463 .write<uint32_t>(0);
464 } else {
465 stream
466 .write<uint32_t>(0)
467 .write<uint32_t>(gameLumpOffset)
468 .write<uint32_t>(curPos - gameLumpOffset);
469 }
470 stream
471 .write<uint32_t>(0)
472 .seek_u(curPos);
473 continue;
474 }
475
476 if (this->stagedLumps.contains(i)) {
477 const auto&[lump, lumpData] = this->stagedLumps.at(i);
478 const auto curPos = stream.tell();
479 stream.seek_u(lumpsHeaderOffset + i * sizeof(Lump));
480 if (!this->l4d2) {
481 stream
482 .write<uint32_t>(curPos)
483 .write<uint32_t>(lumpData.size())
484 .write<uint32_t>(lump.version);
485 } else {
486 stream
487 .write<uint32_t>(lump.version)
488 .write<uint32_t>(curPos)
489 .write<uint32_t>(lumpData.size());
490 }
491 stream
492 .write<uint32_t>(lump.uncompressedLength)
493 .seek_u(curPos)
494 .write(lumpData);
495 continue;
496 }
497
498 if (const auto data = this->getLumpData(static_cast<BSPLump>(i), true)) {
499 const auto curPos = stream.tell();
500 stream.seek_u(lumpsHeaderOffset + i * sizeof(Lump));
501
502 const auto& lump = this->header.lumps[i];
503 if (!this->l4d2) {
504 stream
505 .write<uint32_t>(curPos)
506 .write<uint32_t>(lump.length)
507 .write<uint32_t>(lump.version);
508 } else {
509 stream
510 .write<uint32_t>(lump.version)
511 .write<uint32_t>(curPos)
512 .write<uint32_t>(lump.length);
513 }
514 stream
515 .write<uint32_t>(lump.uncompressedLength)
516 .seek_u(curPos)
517 .write(*data);
518 } else {
519 // We should never be here!
521 }
522 }
523
524 // Lumps are 4 byte aligned
525 if (const auto padding = math::paddingForAlignment(4, stream.tell()); padding > 0) {
526 stream.pad(padding);
527 }
528
529 out.resize(stream.size());
530 if (!fs::writeFileBuffer(outputPath.empty() ? this->path : std::string{outputPath}, out)) {
531 return false;
532 }
533
534 this->path = outputPath;
535 this->stagedLumps.clear();
536 return this->readHeader();
537}
538
540 FileStream reader{this->path};
541 if (!reader) {
542 return false;
543 }
544 reader.seek_in(0);
545
546 if (const auto signature = reader.read<uint32_t>(); signature != BSP_SIGNATURE && signature != BSP_CONSOLE_SIGNATURE) {
547 // File is not a BSP
548 return false;
549 } else if (signature == BSP_CONSOLE_SIGNATURE) {
550 this->console = true;
551 reader.set_big_endian(true);
552 }
553 this->header.version = reader.read<uint32_t>();
554
555 // Contagion funny
556 if (this->header.version == 27) {
557 reader.skip_in<uint32_t>();
558 }
559
560 for (auto& [offset, length, version, uncompressedLength] : this->header.lumps) {
561 reader
562 >> offset
563 >> length
564 >> version
565 >> uncompressedLength;
566 }
567
568 // If no offsets are larger than 1024 (less than the size of the BSP header, but greater
569 // than any lump version), it's probably a L4D2 BSP and those are lump versions!
570 if (this->header.version == 21) {
571 int i;
572 for (i = 0; i < BSP_LUMP_COUNT; i++) {
573 if (this->header.lumps[i].offset > 1024) {
574 break;
575 }
576 }
577 this->l4d2 = i == BSP_LUMP_COUNT;
578 if (this->l4d2) {
579 // Swap fields around
580 for (i = 0; i < BSP_LUMP_COUNT; i++) {
581 std::swap(this->header.lumps[i].offset, this->header.lumps[i].version);
582 std::swap(this->header.lumps[i].offset, this->header.lumps[i].length);
583 }
584 }
585 }
586
587 reader >> this->header.mapRevision;
588 return true;
589}
590
591std::vector<BSPEntityKeyValues> BSP::parseEntities() const {
592 using namespace sourcepp;
593
594 const auto useEscapes = this->getLumpVersion(BSPLump::ENTITIES);
595 if (useEscapes > 1) {
596 return {};
597 }
598
599 auto data = this->getLumpData(BSPLump::ENTITIES);
600 if (!data) {
601 return {};
602 }
603 BufferStreamReadOnly stream{*data};
604
605 std::vector<BSPEntityKeyValues> entities;
606
607 try {
608 while (true) {
609 // Check for EOF - give 3 chars for extra breathing room
610 if (stream.tell() >= stream.size() - 3) {
611 return entities;
612 }
613
614 // Expect an opening brace
616 if (stream.peek<char>() != '{') {
617 break;
618 }
619 stream.skip<char>();
620
621 auto& ent = entities.emplace_back();
622
623 while (true) {
624 // Check if the block is over
626 if (stream.peek<char>() == '}') {
627 stream.skip();
628 break;
629 }
630
631 std::string key, value;
632
633 // Read key
634 {
635 BufferStream keyStream{key};
637 key.resize(keyStream.tell() - 1);
639 }
640
641 // Read value
642 {
643 BufferStream valueStream{value};
645 value.resize(valueStream.tell() - 1);
647 }
648
649 ent[key] = value;
650 }
651 }
652 } catch (const std::overflow_error&) {
653 return {};
654 }
655 return entities;
656}
657
658std::vector<BSPPlane> BSP::parsePlanes() const {
659 return ::parseLumpContents<BSPPlane>(*this, BSPLump::PLANES);
660}
661
662std::vector<BSPTextureData> BSP::parseTextureData() const {
663 return ::parseLumpContents<BSPTextureData>(*this, BSPLump::TEXDATA);
664}
665
666std::vector<BSPVertex> BSP::parseVertices() const {
667 return ::parseLumpContents<BSPVertex>(*this, BSPLump::VERTEXES);
668}
669
670std::vector<BSPNode> BSP::parseNodes() const {
671 return ::parseLumpContents<BSPNode>(*this, BSPLump::NODES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPNode>& out) {
672 if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::NODES); lumpVersion == 1) {
673 stream.read(out, stream.size() / sizeof(BSPNode_v1));
674 } else if (lumpVersion == 0) {
675 ::parseAndUpgrade<BSPNode_v0>(stream, out);
676 }
677 });
678}
679
680std::vector<BSPTextureInfo> BSP::parseTextureInfo() const {
681 return ::parseLumpContents<BSPTextureInfo>(*this, BSPLump::TEXINFO);
682}
683
684std::vector<BSPFace> BSP::parseFaces() const {
685 return ::parseLumpContents<BSPFace>(*this, BSPLump::FACES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPFace>& out) {
686 if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::FACES); lumpVersion == 2) {
687 stream.read(out, stream.size() / sizeof(BSPFace_v2));
688 } else if (lumpVersion == 1) {
689 ::parseAndUpgrade<BSPFace_v1>(stream, out);
690 }
691 });
692}
693
694std::vector<BSPEdge> BSP::parseEdges() const {
695 return ::parseLumpContents<BSPEdge>(*this, BSPLump::EDGES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPEdge>& out) {
696 if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::EDGES); lumpVersion == 1) {
697 stream.read(out, stream.size() / sizeof(BSPEdge_v1));
698 } else if (lumpVersion == 0) {
699 ::parseAndUpgrade<BSPEdge_v0>(stream, out);
700 }
701 });
702}
703
704std::vector<BSPSurfEdge> BSP::parseSurfEdges() const {
705 return ::parseLumpContents<BSPSurfEdge>(*this, BSPLump::SURFEDGES);
706}
707
708std::vector<BSPBrushModel> BSP::parseBrushModels() const {
709 return ::parseLumpContents<BSPBrushModel>(*this, BSPLump::MODELS);
710}
711
712std::vector<BSPFace> BSP::parseOriginalFaces() const {
713 return ::parseLumpContents<BSPFace>(*this, BSPLump::ORIGINALFACES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPFace>& out) {
714 // ORIGINALFACES lump version is always 0?
715 if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::FACES); lumpVersion == 2) {
716 stream.read(out, stream.size() / sizeof(BSPFace_v2));
717 } else if (lumpVersion == 1) {
718 ::parseAndUpgrade<BSPFace_v1>(stream, out);
719 }
720 });
721}
722
723std::vector<BSPGameLump> BSP::parseGameLumps(bool decompress) const {
724 std::vector<BSPGameLump> lumps;
725
726 auto gameLumpData = this->getLumpData(BSPLump::GAME_LUMP);
727 if (!gameLumpData) {
728 return lumps;
729 }
730 BufferStreamReadOnly stream{*gameLumpData};
731 stream.set_big_endian(this->console);
732
733 lumps.resize(stream.read<uint32_t>());
734 for (auto& lump : lumps) {
735 stream
736 .read(lump.signature)
737 .read(lump.isCompressed)
738 .read(lump.version)
739 .read(lump.offset)
740 .read(lump.uncompressedLength);
741 }
742
743 // When one or more game lumps are compressed, the last entry
744 // is empty except the offset field to calculate compressed
745 // size. Game lumps are compressed individually
746 for (uint32_t i = 0; i < lumps.size(); i++) {
747 if (lumps[i].signature == 0) {
748 break;
749 }
750 if (!lumps[i].isCompressed) {
751 lumps[i].data = stream.read_bytes(lumps[i].uncompressedLength);
752 } else {
753 auto nextOffset = lumps[i + 1].offset;
754 if (nextOffset == 0) {
755 static constexpr auto gameLumpID = static_cast<std::underlying_type_t<BSPLump>>(BSPLump::GAME_LUMP);
756 nextOffset = this->header.lumps[gameLumpID].offset + this->header.lumps[gameLumpID].length;
757 }
758 if (!decompress) {
759 lumps[i].data = stream.read_bytes(nextOffset - lumps[i].offset);
760 } else {
761 if (auto uncompressedData = compression::decompressValveLZMA(stream.read_bytes(nextOffset - lumps[i].offset))) {
762 lumps[i].data = *uncompressedData;
763 }
764 }
765 }
766 }
767
768 if (lumps.back().signature == 0) {
769 lumps.pop_back();
770 }
771 return lumps;
772}
#define SOURCEPP_DEBUG_BREAK
Create a breakpoint in debug.
Definition Macros.h:19
Header header
Definition BSP.h:263
auto getLumpData() const
Definition BSP.h:178
uint32_t getVersion() const
Definition BSP.cpp:94
bool hasLump(BSPLump lumpIndex) const
Definition BSP.cpp:126
std::vector< BSPBrushModel > parseBrushModels() const
Definition BSP.cpp:708
bool console
Definition BSP.h:273
std::vector< BSPNode > parseNodes() const
Definition BSP.cpp:670
std::vector< BSPTextureData > parseTextureData() const
Definition BSP.cpp:662
std::vector< BSPGameLump > parseGameLumps(bool decompress) const
Definition BSP.cpp:723
std::unordered_map< uint32_t, std::pair< Lump, std::vector< std::byte > > > stagedLumps
Definition BSP.h:266
std::vector< BSPFace > parseOriginalFaces() const
Definition BSP.cpp:712
void setL4D2(bool isL4D2)
Definition BSP.cpp:114
static BSP create(std::string path, uint32_t version=21, uint32_t mapRevision=0)
Definition BSP.cpp:75
bool isLumpCompressed(BSPLump lumpIndex) const
Definition BSP.cpp:135
uint32_t stagedVersion
Definition BSP.h:265
void reset()
Resets ALL in-memory modifications (version, all lumps including game lumps, map revision)
Definition BSP.cpp:295
void setConsole(bool isConsole)
Definition BSP.cpp:122
bool bake(std::string_view outputPath="")
Definition BSP.cpp:365
void resetLump(BSPLump lumpIndex)
Reset changes made to a lump before they're written to disk.
Definition BSP.cpp:277
uint32_t getMapRevision() const
Definition BSP.cpp:102
std::vector< BSPGameLump > stagedGameLumps
Definition BSP.h:267
bool setGameLump(BSPGameLump::Signature signature, uint16_t version, std::span< const std::byte > data, uint8_t compressLevel=0)
Definition BSP.cpp:254
std::vector< BSPFace > parseFaces() const
Definition BSP.cpp:684
uint32_t stagedMapRevision
Definition BSP.h:268
BSP(std::string path_, bool loadPatchFiles=true)
Definition BSP.cpp:46
void setVersion(uint32_t version)
Definition BSP.cpp:98
std::vector< BSPSurfEdge > parseSurfEdges() const
Definition BSP.cpp:704
std::optional< std::vector< std::byte > > getGameLumpData(BSPGameLump::Signature signature) const
Definition BSP.cpp:242
bool isL4D2() const
Definition BSP.cpp:110
std::vector< BSPEdge > parseEdges() const
Definition BSP.cpp:694
std::vector< BSPVertex > parseVertices() const
Definition BSP.cpp:666
std::optional< std::vector< std::byte > > getLumpData(BSPLump lumpIndex, bool noDecompression=false) const
Definition BSP.cpp:154
void createLumpPatchFile(BSPLump lumpIndex) const
Definition BSP.cpp:305
bool readHeader()
Definition BSP.cpp:539
uint16_t getGameLumpVersion(BSPGameLump::Signature signature) const
Definition BSP.cpp:233
uint32_t getLumpVersion(BSPLump lumpIndex) const
Definition BSP.cpp:143
bool isGameLumpCompressed(BSPGameLump::Signature signature) const
Definition BSP.cpp:224
bool l4d2
Definition BSP.h:271
std::vector< BSPPlane > parsePlanes() const
Definition BSP.cpp:658
bool setLumpFromPatchFile(const std::string &lumpFilePath)
Definition BSP.cpp:343
std::string path
Definition BSP.h:262
bool setLump(BSPLump lumpIndex, uint32_t version, std::span< const std::byte > data, uint8_t compressLevel=0)
BSP::setGameLump should be used for writing game lumps as they need special handling.
Definition BSP.cpp:177
bool isConsole() const
Definition BSP.cpp:118
std::vector< BSPTextureInfo > parseTextureInfo() const
Definition BSP.cpp:680
std::vector< BSPEntityKeyValues > parseEntities() const
Definition BSP.cpp:591
void setMapRevision(uint32_t mapRevision)
Definition BSP.cpp:106
Definition BSP.h:18
constexpr std::array< uint32_t, 64 > BSP_LUMP_ORDER
Pulled from Portal 2, map e1912.
Definition BSP.h:114
constexpr auto BSP_SIGNATURE
Definition BSP.h:20
constexpr int32_t BSP_LUMP_COUNT
Definition BSP.h:108
BSPLump
Definition BSP.h:23
@ ORIGINALFACES
Definition BSP.h:60
@ GAME_LUMP
Definition BSP.h:69
@ SURFEDGES
Definition BSP.h:38
constexpr auto BSP_CONSOLE_SIGNATURE
Definition BSP.h:21
std::optional< std::vector< std::byte > > compressValveLZMA(std::span< const std::byte > data, uint8_t compressLevel=6)
Definition LZMA.cpp:8
std::optional< std::vector< std::byte > > decompressValveLZMA(std::span< const std::byte > data)
Definition LZMA.cpp:51
bool writeFileBuffer(const std::filesystem::path &filepath, std::span< const std::byte > buffer)
Definition FS.cpp:25
constexpr uint16_t paddingForAlignment(uint16_t alignment, uint64_t n)
Definition Math.h:61
void eatWhitespaceAndSingleLineComments(BufferStream &stream, std::string_view singleLineCommentStart=DEFAULT_SINGLE_LINE_COMMENT_START)
Eat all whitespace and single line comments after the current stream position.
Definition Text.cpp:108
constexpr std::string_view DEFAULT_STRING_END
Definition Text.h:15
constexpr std::string_view DEFAULT_STRING_START
Definition Text.h:14
const EscapeSequenceMap & getDefaultEscapeSequencesOrNone(bool useEscapes)
Definition Text.cpp:27
std::string_view readStringToBuffer(BufferStream &stream, BufferStream &backing, std::string_view start=DEFAULT_STRING_START, std::string_view end=DEFAULT_STRING_END, const EscapeSequenceMap &escapeSequences=getDefaultEscapeSequences())
Read a string starting at the current stream position.
Definition Text.cpp:180
std::vector< std::byte > data
Definition LumpData.h:257