10#include <FileStream.h>
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));
27 BufferStreamReadOnly stream{*data};
30 callback(bsp, stream, out);
34template<BufferStreamPODType Old, BufferStreamPODType New>
35requires requires(New) { {New::upgrade(Old{})} -> std::same_as<New>; }
36void parseAndUpgrade(BufferStreamReadOnly& stream, std::vector<New>& out) {
38 stream.read(old, stream.size() /
sizeof(Old));
39 for (
const auto& elem : old) {
40 out.push_back(New::upgrade(elem));
46BSP::BSP(std::string path_,
bool loadPatchFiles)
47 :
path(std::move(path_)) {
58 const auto fsPath = std::filesystem::path{this->
path};
59 const auto fsStem = (fsPath.parent_path() / fsPath.stem()).string() +
"_l_";
61 for (
int i = 0; ; i++) {
62 auto patchFilePath = fsStem + std::to_string(i) +
".lmp";
63 if (!std::filesystem::exists(patchFilePath)) {
71BSP::operator bool()
const {
72 return !this->
path.empty();
77 FileStream writer{
path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
80 writer.write<uint32_t>(0);
89 writer << mapRevision;
127 if (this->
path.empty()) {
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);
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;
144 if (this->
path.empty()) {
147 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
151 return this->
header.lumps[lump].version;
155 if (this->
path.empty() || !this->hasLump(lumpIndex)) {
159 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
160 std::vector<std::byte> lumpBytes;
165 FileStream reader{this->
path};
167 .seek_in(this->
header.lumps[lump].offset)
168 .read_bytes(this->
header.lumps[lump].length);
177bool BSP::setLump(
BSPLump lumpIndex, uint32_t version, std::span<const std::byte> data, uint8_t compressLevel) {
191 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
192 if (compressLevel > 0) {
194 if (!compressedData) {
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()});
201 this->
stagedLumps[lump] = std::make_pair<Lump, std::vector<std::byte>>({
202 .version = version, .uncompressedLength = 0,
203 }, {data.begin(), data.end()});
208bool BSP::setLump(uint32_t version, std::span<const BSPEntityKeyValues> data, uint8_t compressLevel) {
213 for (
const auto& ent : data) {
214 out += ent.bake(version == 1) +
'\n';
221 return this->
setLump(
BSPLump::ENTITIES, version, {
reinterpret_cast<const std::byte*
>(out.data()), out.size()}, compressLevel);
226 if (gameLump.signature == signature) {
227 return gameLump.isCompressed;
235 if (gameLump.signature == signature) {
236 return gameLump.version;
244 if (gameLump.signature == signature) {
245 if (gameLump.isCompressed) {
248 return gameLump.data;
256 .signature = signature,
257 .isCompressed = compressLevel > 0,
260 .uncompressedLength =
static_cast<uint32_t
>(data.size()),
265 if (!compressedData) {
268 gameLump.
data = *compressedData;
270 gameLump.
data = {data.begin(), data.end()};
278 if (this->
path.empty()) {
289 const auto lump =
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex);
296 if (this->
path.empty()) {
306 if (this->
path.empty()) {
310 const auto lumpData = this->
getLumpData(lumpIndex);
319 lumpUncompressedLength
320 ] = this->
header.lumps.at(
static_cast<std::underlying_type_t<BSPLump>
>(lumpIndex));
322 const auto fsPath = std::filesystem::path{this->
path};
323 const auto fsStem = (fsPath.parent_path() / fsPath.stem()).string() +
"_l_";
324 int nonexistentNumber = 0;
326 if (!std::filesystem::exists(fsStem + std::to_string(nonexistentNumber) +
".lmp")) {
332 FileStream writer{fsStem + std::to_string(nonexistentNumber) +
".lmp", FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT};
335 .write<int32_t>(
sizeof(int32_t) * 5)
339 .write(this->
header.mapRevision)
344 if (this->
path.empty()) {
348 FileStream reader{lumpFilePath};
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>();
361 this->
setLump(
static_cast<BSPLump>(index), version, reader.seek_in(offset).read_bytes(length));
366 if (this->
path.empty()) {
370 std::vector<std::byte> out;
371 BufferStream stream{out};
372 stream.set_big_endian(this->
console);
375 if (this->stagedVersion == 27) {
377 stream.write<uint32_t>(0);
380 const auto lumpsHeaderOffset = stream.tell();
381 for (
int i = 0; i <
sizeof(Header::lumps); i++) {
382 stream.write<uint8_t>(0);
389 const auto curPos = stream.tell();
390 stream.seek_u(lumpsHeaderOffset + i *
sizeof(Lump));
392 const auto& lump = this->
header.lumps[i];
397 .write<uint32_t>(lump.version);
400 .write<uint32_t>(lump.version)
416 const auto gameLumpOffset = stream.tell();
418 bool oneOrMoreGameLumpCompressed =
false;
420 if (gameLump.isCompressed) {
421 oneOrMoreGameLumpCompressed =
true;
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);
429 for (
const auto& gameLump : this->stagedGameLumps) {
430 if (gameLump.signature == 0) {
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();
441 if (oneOrMoreGameLumpCompressed) {
446 .write<uint32_t>(gameLumpCurrentOffset)
450 for (
const auto& gameLump : this->stagedGameLumps) {
451 if (gameLump.signature == 0) {
454 stream.write(gameLump.data);
457 const auto curPos = stream.tell();
458 stream.seek_u(lumpsHeaderOffset + i *
sizeof(Lump));
461 .write<uint32_t>(gameLumpOffset)
462 .write<uint32_t>(curPos - gameLumpOffset)
467 .write<uint32_t>(gameLumpOffset)
468 .write<uint32_t>(curPos - gameLumpOffset);
477 const auto&[lump, lumpData] = this->
stagedLumps.at(i);
478 const auto curPos = stream.tell();
479 stream.seek_u(lumpsHeaderOffset + i *
sizeof(Lump));
482 .write<uint32_t>(curPos)
483 .write<uint32_t>(lumpData.size())
484 .write<uint32_t>(lump.version);
487 .write<uint32_t>(lump.version)
488 .write<uint32_t>(curPos)
489 .write<uint32_t>(lumpData.size());
492 .write<uint32_t>(lump.uncompressedLength)
499 const auto curPos = stream.tell();
500 stream.seek_u(lumpsHeaderOffset + i *
sizeof(Lump));
502 const auto& lump = this->
header.lumps[i];
505 .write<uint32_t>(curPos)
506 .write<uint32_t>(lump.length)
507 .write<uint32_t>(lump.version);
510 .write<uint32_t>(lump.version)
511 .write<uint32_t>(curPos)
512 .write<uint32_t>(lump.length);
515 .write<uint32_t>(lump.uncompressedLength)
529 out.resize(stream.size());
530 if (!
fs::writeFileBuffer(outputPath.empty() ? this->path : std::string{outputPath}, out)) {
534 this->
path = outputPath;
540 FileStream reader{this->
path};
551 reader.set_big_endian(
true);
553 this->
header.version = reader.read<uint32_t>();
556 if (this->
header.version == 27) {
557 reader.skip_in<uint32_t>();
560 for (
auto& [offset, length, version, uncompressedLength] : this->
header.lumps) {
565 >> uncompressedLength;
570 if (this->
header.version == 21) {
573 if (this->
header.lumps[i].offset > 1024) {
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);
587 reader >> this->
header.mapRevision;
595 if (useEscapes > 1) {
603 BufferStreamReadOnly stream{*data};
605 std::vector<BSPEntityKeyValues> entities;
610 if (stream.tell() >= stream.size() - 3) {
616 if (stream.peek<
char>() !=
'{') {
621 auto& ent = entities.emplace_back();
626 if (stream.peek<
char>() ==
'}') {
631 std::string key, value;
635 BufferStream keyStream{key};
637 key.resize(keyStream.tell() - 1);
643 BufferStream valueStream{value};
645 value.resize(valueStream.tell() - 1);
652 }
catch (
const std::overflow_error&) {
671 return ::parseLumpContents<BSPNode>(*
this,
BSPLump::NODES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPNode>& out) {
673 stream.read(out, stream.size() /
sizeof(
BSPNode_v1));
674 }
else if (lumpVersion == 0) {
675 ::parseAndUpgrade<BSPNode_v0>(stream, out);
685 return ::parseLumpContents<BSPFace>(*
this,
BSPLump::FACES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPFace>& out) {
687 stream.read(out, stream.size() /
sizeof(
BSPFace_v2));
688 }
else if (lumpVersion == 1) {
689 ::parseAndUpgrade<BSPFace_v1>(stream, out);
695 return ::parseLumpContents<BSPEdge>(*
this,
BSPLump::EDGES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPEdge>& out) {
697 stream.read(out, stream.size() /
sizeof(
BSPEdge_v1));
698 }
else if (lumpVersion == 0) {
699 ::parseAndUpgrade<BSPEdge_v0>(stream, out);
713 return ::parseLumpContents<BSPFace>(*
this,
BSPLump::ORIGINALFACES, [](
const BSP& bsp, BufferStreamReadOnly& stream, std::vector<BSPFace>& out) {
716 stream.read(out, stream.size() /
sizeof(
BSPFace_v2));
717 }
else if (lumpVersion == 1) {
718 ::parseAndUpgrade<BSPFace_v1>(stream, out);
724 std::vector<BSPGameLump> lumps;
730 BufferStreamReadOnly stream{*gameLumpData};
731 stream.set_big_endian(this->
console);
733 lumps.resize(stream.read<uint32_t>());
734 for (
auto& lump : lumps) {
736 .read(lump.signature)
737 .read(lump.isCompressed)
740 .read(lump.uncompressedLength);
746 for (uint32_t i = 0; i < lumps.size(); i++) {
747 if (lumps[i].signature == 0) {
750 if (!lumps[i].isCompressed) {
751 lumps[i].data = stream.read_bytes(lumps[i].uncompressedLength);
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;
759 lumps[i].data = stream.read_bytes(nextOffset - lumps[i].offset);
762 lumps[i].data = *uncompressedData;
768 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
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
uint16_t getGameLumpVersion(BSPGameLump::Signature signature) 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::filesystem::path &filepath, std::span< const std::byte > buffer)
constexpr uint16_t paddingForAlignment(uint16_t alignment, uint64_t n)
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.
constexpr std::string_view DEFAULT_STRING_END
constexpr std::string_view DEFAULT_STRING_START
const EscapeSequenceMap & getDefaultEscapeSequencesOrNone(bool useEscapes)
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.
std::vector< std::byte > data