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));
25 BufferStreamReadOnly stream{*data};
28 callback(bsp, stream, out);
32template<BufferStreamPODType Old, BufferStreamPODType New>
33requires requires(New) { {New::upgrade(Old{})} -> std::same_as<New>; }
34void parseAndUpgrade(BufferStreamReadOnly& stream, std::vector<New>& out) {
36 stream.read(old, stream.size() /
sizeof(Old));
37 for (
const auto& elem : old) {
38 out.push_back(New::upgrade(elem));
44BSP::BSP(std::string path_,
bool loadPatchFiles)
45 : path(std::move(path_)) {
56 const auto fsPath = std::filesystem::path{this->
path};
57 const auto fsStem = (fsPath.parent_path() / fsPath.stem()).
string() +
"_l_";
59 for (
int i = 0; ; i++) {
60 auto patchFilePath = fsStem + std::to_string(i) +
".lmp";
61 if (!std::filesystem::exists(patchFilePath)) {
69BSP::operator bool()
const {
70 return !this->path.empty();
75 FileStream writer{
path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
78 writer.write<uint32_t>(0);
87 writer << mapRevision;
125 if (this->
path.empty()) {
128 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
130 || (this->
header.lumps[lump].length != 0 && this->header.lumps[lump].offset != 0);
134 if (this->
hasLump(lumpIndex)) {
135 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
137 || (this->
header.lumps[lump].uncompressedLength > 0);
143 if (this->
path.empty()) {
146 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
150 return this->
header.lumps[lump].version;
154 if (this->
path.empty() || !this->hasLump(lumpIndex)) {
158 auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
159 std::vector<std::byte> lumpBytes;
164 FileStream reader{this->
path};
166 .seek_in(this->
header.lumps[lump].offset)
167 .read_bytes(this->
header.lumps[lump].length);
176bool BSP::setLump(
BSPLump lumpIndex, uint32_t version, std::span<const std::byte> data, uint8_t compressLevel) {
190 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
191 if (compressLevel > 0) {
193 if (!compressedData) {
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()});
200 this->
stagedLumps[lump] = std::make_pair<Lump, std::vector<std::byte>>({
201 .version = version, .uncompressedLength = 0,
202 }, {data.begin(), data.end()});
207bool BSP::setLump(uint32_t version, std::span<const BSPEntityKeyValues> data, uint8_t compressLevel) {
212 for (
const auto& ent : data) {
213 out += ent.bake(version == 1) +
'\n';
220 return this->
setLump(
BSPLump::ENTITIES, version, {
reinterpret_cast<const std::byte*
>(out.data()), out.size()}, compressLevel);
225 if (gameLump.signature == signature) {
226 return gameLump.isCompressed;
234 if (gameLump.signature == signature) {
235 return gameLump.version;
243 if (gameLump.signature == signature) {
244 if (gameLump.isCompressed) {
247 return gameLump.data;
256 .isCompressed = compressLevel > 0,
259 .uncompressedLength =
static_cast<uint32_t
>(data.size()),
264 if (!compressedData) {
267 gameLump.data = *compressedData;
269 gameLump.data = {data.begin(), data.end()};
277 if (this->
path.empty()) {
288 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
295 if (this->
path.empty()) {
305 if (this->
path.empty()) {
318 lumpUncompressedLength
319 ] = this->
header.lumps.at(
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex));
321 const auto fsPath = std::filesystem::path{this->
path};
322 const auto fsStem = (fsPath.parent_path() / fsPath.stem()).
string() +
"_l_";
323 int nonexistentNumber = 0;
325 if (!std::filesystem::exists(fsStem + std::to_string(nonexistentNumber) +
".lmp")) {
331 FileStream writer{fsStem + std::to_string(nonexistentNumber) +
".lmp", FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
334 .write<int32_t>(
sizeof(int32_t) * 5)
338 .write(this->
header.mapRevision)
343 if (this->
path.empty()) {
347 FileStream reader{lumpFilePath};
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>();
360 this->
setLump(
static_cast<BSPLump>(index), version, reader.seek_in(offset).read_bytes(length));
365 if (this->
path.empty()) {
369 std::vector<std::byte> out;
370 BufferStream stream{out};
371 stream.set_big_endian(this->
console);
374 if (this->stagedVersion == 27) {
376 stream.write<uint32_t>(0);
379 const auto lumpsHeaderOffset = stream.tell();
380 for (
int i = 0; i <
sizeof(Header::lumps); i++) {
381 stream.write<uint8_t>(0);
393 for (
int p = 0; p < padding; p++) {
394 stream.write<uint8_t>(0);
399 const auto gameLumpOffset = stream.tell();
401 bool oneOrMoreGameLumpCompressed =
false;
403 if (gameLump.isCompressed) {
404 oneOrMoreGameLumpCompressed =
true;
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);
412 for (
const auto& gameLump : this->stagedGameLumps) {
413 if (gameLump.signature == 0) {
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();
424 if (oneOrMoreGameLumpCompressed) {
433 for (
const auto& gameLump : this->stagedGameLumps) {
434 if (gameLump.signature == 0) {
437 stream.write(gameLump.data);
440 const auto curPos = stream.tell();
441 stream.seek_u(lumpsHeaderOffset + (i *
sizeof(Lump)));
444 .write<uint32_t>(gameLumpOffset)
445 .write<uint32_t>(curPos - gameLumpOffset)
450 .write<uint32_t>(gameLumpOffset)
451 .write<uint32_t>(curPos - gameLumpOffset);
461 const auto curPos = stream.tell();
462 stream.seek_u(lumpsHeaderOffset + (i *
sizeof(Lump)));
465 .write<uint32_t>(curPos)
466 .write<uint32_t>(lumpPair.second.size())
467 .write<uint32_t>(lumpPair.first.version);
470 .write<uint32_t>(lumpPair.first.version)
471 .write<uint32_t>(curPos)
472 .write<uint32_t>(lumpPair.second.size());
475 .write<uint32_t>(lumpPair.first.uncompressedLength)
477 .write(lumpPair.second);
483 const auto curPos = stream.tell();
484 stream.seek_u(lumpsHeaderOffset + (i *
sizeof(Lump)));
486 auto& lump = this->
header.lumps[i];
489 .write<uint32_t>(curPos)
490 .write<uint32_t>(lump.length)
491 .write<uint32_t>(lump.version);
494 .write<uint32_t>(lump.version)
495 .write<uint32_t>(curPos)
496 .write<uint32_t>(lump.length);
499 .write<uint32_t>(lump.uncompressedLength)
510 for (
int p = 0; p < padding; p++) {
511 stream.write<uint8_t>(0);
515 out.resize(stream.size());
520 this->
path = outputPath;
526 FileStream reader{this->
path};
537 reader.set_big_endian(
true);
539 this->
header.version = reader.read<uint32_t>();
542 if (this->
header.version == 27) {
543 reader.skip_in<uint32_t>();
546 for (
auto& lump : this->
header.lumps) {
551 >> lump.uncompressedLength;
556 if (this->
header.version == 21) {
559 if (this->
header.lumps[i].offset > 1024) {
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);
573 reader >> this->
header.mapRevision;
581 if (useEscapes > 1) {
589 BufferStreamReadOnly stream{*data};
591 std::vector<BSPEntityKeyValues> entities;
596 if (stream.tell() >= stream.size() - 3) {
602 if (stream.peek<
char>() !=
'{') {
607 auto& ent = entities.emplace_back();
612 if (stream.peek<
char>() ==
'}') {
617 std::string key, value;
621 BufferStream keyStream{key};
623 key.resize(keyStream.tell() - 1);
629 BufferStream valueStream{value};
631 value.resize(valueStream.tell() - 1);
638 }
catch (
const std::overflow_error&) {
657 return ::parseLumpContents<BSPNode>(*
this,
BSPLump::NODES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPNode>& out) {
659 stream.read(out, stream.size() / sizeof(BSPNode_v1));
660 }
else if (lumpVersion == 0) {
661 ::parseAndUpgrade<BSPNode_v0>(stream, out);
671 return ::parseLumpContents<BSPFace>(*
this,
BSPLump::FACES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPFace>& out) {
673 stream.read(out, stream.size() / sizeof(BSPFace_v2));
674 }
else if (lumpVersion == 1) {
675 ::parseAndUpgrade<BSPFace_v1>(stream, out);
681 return ::parseLumpContents<BSPEdge>(*
this,
BSPLump::EDGES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPEdge>& out) {
683 stream.read(out, stream.size() / sizeof(BSPEdge_v1));
684 }
else if (lumpVersion == 0) {
685 ::parseAndUpgrade<BSPEdge_v0>(stream, out);
699 return ::parseLumpContents<BSPFace>(*
this,
BSPLump::ORIGINALFACES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPFace>& out) {
702 stream.read(out, stream.size() / sizeof(BSPFace_v2));
703 }
else if (lumpVersion == 1) {
704 ::parseAndUpgrade<BSPFace_v1>(stream, out);
710 std::vector<BSPGameLump> lumps;
716 BufferStreamReadOnly stream{*gameLumpData};
717 stream.set_big_endian(this->
console);
719 lumps.resize(stream.read<uint32_t>());
720 for (
auto& lump : lumps) {
722 .read(lump.signature)
723 .read(lump.isCompressed)
726 .read(lump.uncompressedLength);
732 for (uint32_t i = 0; i < lumps.size(); i++) {
733 if (lumps[i].signature == 0) {
736 if (!lumps[i].isCompressed) {
737 lumps[i].data = stream.read_bytes(lumps[i].uncompressedLength);
739 auto nextOffset = lumps[i + 1].offset;
740 if (nextOffset == 0) {
742 nextOffset = this->
header.lumps[id].offset + this->
header.lumps[id].length;
745 lumps[i].data = stream.read_bytes(nextOffset - lumps[i].offset);
748 if (uncompressedData) {
749 lumps[i].data = *uncompressedData;
755 if (lumps.back().signature == 0) {
#define SOURCEPP_DEBUG_BREAK
Create a breakpoint in debug.
uint32_t getVersion() const
bool hasLump(BSPLump lumpIndex) const
std::vector< BSPBrushModel > parseBrushModels() const
std::vector< BSPNode > parseNodes() const
std::vector< BSPTextureData > parseTextureData() const
std::vector< BSPGameLump > parseGameLumps(bool decompress) const
std::unordered_map< uint32_t, std::pair< Lump, std::vector< std::byte > > > stagedLumps
std::vector< BSPFace > parseOriginalFaces() const
void setL4D2(bool isL4D2)
static BSP create(std::string path, uint32_t version=21, uint32_t mapRevision=0)
bool isLumpCompressed(BSPLump lumpIndex) const
void reset()
Resets ALL in-memory modifications (version, all lumps including game lumps, map revision)
void setConsole(bool isConsole)
bool bake(std::string_view outputPath="")
void resetLump(BSPLump lumpIndex)
Reset changes made to a lump before they're written to disk.
uint32_t getMapRevision() const
std::vector< BSPGameLump > stagedGameLumps
bool setGameLump(BSPGameLump::Signature signature, uint16_t version, std::span< const std::byte > data, uint8_t compressLevel=0)
std::vector< BSPFace > parseFaces() const
uint32_t stagedMapRevision
BSP(std::string path_, bool loadPatchFiles=true)
void setVersion(uint32_t version)
std::vector< BSPSurfEdge > parseSurfEdges() const
std::optional< std::vector< std::byte > > getGameLumpData(BSPGameLump::Signature signature) const
uint16_t getGameLumpVersion(BSPGameLump::Signature signature)
std::vector< BSPEdge > parseEdges() const
std::vector< BSPVertex > parseVertices() const
std::optional< std::vector< std::byte > > getLumpData(BSPLump lumpIndex, bool noDecompression=false) const
void createLumpPatchFile(BSPLump lumpIndex) const
uint32_t getLumpVersion(BSPLump lumpIndex) const
bool isGameLumpCompressed(BSPGameLump::Signature signature) const
std::vector< BSPPlane > parsePlanes() const
bool setLumpFromPatchFile(const std::string &lumpFilePath)
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.
std::vector< BSPTextureInfo > parseTextureInfo() const
std::vector< BSPEntityKeyValues > parseEntities() const
void setMapRevision(uint32_t mapRevision)
constexpr std::array< uint32_t, 64 > BSP_LUMP_ORDER
Pulled from Portal 2, map e1912.
constexpr auto BSP_SIGNATURE
constexpr int32_t BSP_LUMP_COUNT
constexpr auto BSP_CONSOLE_SIGNATURE
std::optional< std::vector< std::byte > > compressValveLZMA(std::span< const std::byte > data, uint8_t compressLevel=6)
std::optional< std::vector< std::byte > > decompressValveLZMA(std::span< const std::byte > data)
bool writeFileBuffer(const std::string &filepath, std::span< const std::byte > buffer)
constexpr uint16_t paddingForAlignment(uint16_t alignment, uint64_t n)
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.
const EscapeSequenceMap NO_ESCAPE_SEQUENCES
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.
const EscapeSequenceMap DEFAULT_ESCAPE_SEQUENCES
constexpr std::string_view DEFAULT_STRING_END
constexpr std::string_view DEFAULT_STRING_START
std::vector< std::byte > data
enum bsppp::BSPGameLump::Signature signature