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