diff --git a/include/bsppp/BSP.h b/include/bsppp/BSP.h new file mode 100644 index 000000000..0a31451e1 --- /dev/null +++ b/include/bsppp/BSP.h @@ -0,0 +1,269 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "EntityLump.h" +#include "LumpData.h" +#include "PakLump.h" + +namespace bsppp { + +constexpr auto BSP_SIGNATURE = sourcepp::parser::binary::makeFourCC("VBSP"); +constexpr auto LZMA_VALVE_SIGNATURE = sourcepp::parser::binary::makeFourCC("LZMA"); + +enum class BSPLump : int32_t { + UNKNOWN = -1, + ENTITIES = 0, + PLANES, + TEXDATA, + VERTEXES, + VISIBILITY, + NODES, + TEXINFO, + FACES, + LIGHTING, + OCCLUSION, + LEAFS, + FACEIDS, + EDGES, + SURFEDGES, + MODELS, + WORLDLIGHTS, + LEAFFACES, + LEAFBRUSHES, + BRUSHES, + BRUSHSIDES, + AREAS, + AREAPORTALS, + S2004_PORTALS, + UNUSED0 = S2004_PORTALS, + SL4D2_PROPCOLLISION = S2004_PORTALS, + S2004_CLUSTERS, + UNUSED1 = S2004_CLUSTERS, + SL4D2_PROPHULLS = S2004_CLUSTERS, + S2004_PORTALVERTS, + UNUSED2 = S2004_PORTALVERTS, + SL4D2_PROPHULLVERTS = S2004_PORTALVERTS, + S2004_CLUSTERPORTALS, + UNUSED3 = S2004_CLUSTERPORTALS, + SL4D2_PROPTRIS = S2004_CLUSTERPORTALS, + DISPINFO, + ORIGINALFACES, + PHYSDISP, + PHYSCOLLIDE, + VERTNORMALS, + VERTNORMALINDICES, + S2004_DISP_LIGHTMAP_ALPHAS, + UNUSED4 = S2004_DISP_LIGHTMAP_ALPHAS, + DISP_VERTS, + DISP_LIGHTMAP_SAMPLE_POSITIONS, + GAME_LUMP, + LEAFWATERDATA, + PRIMITIVES, + PRIMVERTS, + PRIMINDICES, + PAKFILE, + CLIPPORTALVERTS, + CUBEMAPS, + TEXDATA_STRING_DATA, + TEXDATA_STRING_TABLE, + OVERLAYS, + LEAFMINDISTTOWATER, + FACE_MACRO_TEXTURE_INFO, + DISP_TRIS, + S2004_PHYSCOLLIDESURFACE, + UNUSED5 = S2004_PHYSCOLLIDESURFACE, + SL4D2_PROP_BLOB = S2004_PHYSCOLLIDESURFACE, + WATEROVERLAYS, + S2006_XBOX_LIGHTMAPPAGES, + LEAF_AMBIENT_INDEX_HDR = S2006_XBOX_LIGHTMAPPAGES, + S2006_XBOX_LIGHTMAPPAGEINFOS, + LEAF_AMBIENT_INDEX = S2006_XBOX_LIGHTMAPPAGEINFOS, + LIGHTING_HDR, + WORLDLIGHTS_HDR, + LEAF_AMBIENT_LIGHTING_HDR, + LEAF_AMBIENT_LIGHTING, + XBOX_XZIPPAKFILE, + FACES_HDR, + MAP_FLAGS, + OVERLAY_FADES, + L4D_OVERLAY_SYSTEM_LEVELS, + UNUSED6 = L4D_OVERLAY_SYSTEM_LEVELS, + L4D2_PHYSLEVEL, + UNUSED7 = L4D2_PHYSLEVEL, + ASW_DISP_MULTIBLEND, + UNUSED8 = ASW_DISP_MULTIBLEND, + + COUNT, +}; +constexpr int32_t BSP_LUMP_COUNT = 64; +static_assert(static_cast>(BSPLump::COUNT) == BSP_LUMP_COUNT, "Incorrect lump count!"); + +/// Pulled from Portal 2, map e1912. This is not a given for every game or even map, and obviously lump order doesn't +/// matter one bit, but we do at least want the paklump to be at the end since it commonly grows and shrinks. +constexpr std::array BSP_LUMP_ORDER{ + 25, 24, 32, 57, 49, 59, 6, 2, + 43, 44, 10, 17, 1, 18, 19, 14, + 5, 20, 21, 4, 0, 29, 26, 62, + 3, 12, 13, 7, 58, 33, 48, 63, + 28, 9, 8, 53, 37, 38, 39, 30, + 31, 56, 52, 51, 55, 16, 36, 45, + 50, 60, 61, 46, 42, 41, 54, 15, + 34, 47, 11, 22, 23, 27, 35, 40, +}; + +class BSP { + struct Lump { + /// Byte offset into file + uint32_t offset; + /// Length of lump data + uint32_t length; + /// Lump format version + uint32_t version; + /// Uncompressed length if lump is compressed, else 0 + uint32_t uncompressedLength; + }; + + struct Header { + /// Version of the BSP file + uint32_t version; + /// Lump metadata + std::array lumps; + /// Map version number + uint32_t mapRevision; + }; + +public: + explicit BSP(std::string path_, bool loadPatchFiles = true); + + explicit operator bool() const; + + static BSP create(std::string path, uint32_t version = 21, uint32_t mapRevision = 0); + + [[nodiscard]] uint32_t getVersion() const; + + void setVersion(uint32_t version); + + [[nodiscard]] uint32_t getMapRevision() const; + + void setMapRevision(uint32_t mapRevision); + + [[nodiscard]] bool hasLump(BSPLump lumpIndex) const; + + [[nodiscard]] bool isLumpCompressed(BSPLump lumpIndex) const; + + [[nodiscard]] uint32_t getLumpVersion(BSPLump lumpIndex) const; + + [[nodiscard]] std::optional> getLumpData(BSPLump lumpIndex, bool noDecompression = false) const; + + template + [[nodiscard]] auto getLumpData() const { + if constexpr (Lump == BSPLump::ENTITIES) { + return this->parseEntities(); + } else if constexpr (Lump == BSPLump::PLANES) { + return this->parsePlanes(); + } else if constexpr (Lump == BSPLump::TEXDATA) { + return this->parseTextureData(); + } else if constexpr (Lump == BSPLump::VERTEXES) { + return this->parseVertices(); + } else if constexpr (Lump == BSPLump::NODES) { + return this->parseNodes(); + } else if constexpr (Lump == BSPLump::TEXINFO) { + return this->parseTextureInfo(); + } else if constexpr (Lump == BSPLump::FACES) { + return this->parseFaces(); + } else if constexpr (Lump == BSPLump::EDGES) { + return this->parseEdges(); + } else if constexpr (Lump == BSPLump::SURFEDGES) { + return this->parseSurfEdges(); + } else if constexpr (Lump == BSPLump::MODELS) { + return this->parseBrushModels(); + } else if constexpr (Lump == BSPLump::ORIGINALFACES) { + return this->parseOriginalFaces(); + } else if constexpr (Lump == BSPLump::GAME_LUMP) { + return this->parseGameLumps(true); + } else { + return this->getLumpData(Lump); + } + } + + /// BSP::setGameLump should be used for writing game lumps as they need special handling. Paklump can be + /// written here but compression is unsupported, prefer using bsppp::PakLump or your favorite zip library + /// instead. Valid compressLevel range is 0 to 9, 0 is considered off, 9 the slowest and most compressiest + bool setLump(BSPLump lumpIndex, uint32_t version, std::span data, uint8_t compressLevel = 0); + + bool setLump(uint32_t version, std::span data, uint8_t compressLevel = 0); + + [[nodiscard]] bool isGameLumpCompressed(BSPGameLump::Signature signature) const; + + [[nodiscard]] uint16_t getGameLumpVersion(BSPGameLump::Signature signature); + + [[nodiscard]] std::optional> getGameLumpData(BSPGameLump::Signature signature) const; + + bool setGameLump(BSPGameLump::Signature signature, uint16_t version, std::span data, uint8_t compressLevel = 0); + + /// Reset changes made to a lump before they're written to disk + void resetLump(BSPLump lumpIndex); + + /// Resets ALL in-memory modifications (version, all lumps including game lumps, map revision) + void reset(); + + void createLumpPatchFile(BSPLump lumpIndex) const; + + bool setLumpFromPatchFile(const std::string& lumpFilePath); + + bool bake(std::string_view outputPath = ""); + +protected: + bool readHeader(); + + [[nodiscard]] std::vector parseEntities() const; + + [[nodiscard]] std::vector parsePlanes() const; + + [[nodiscard]] std::vector parseTextureData() const; + + [[nodiscard]] std::vector parseVertices() const; + + [[nodiscard]] std::vector parseNodes() const; + + [[nodiscard]] std::vector parseTextureInfo() const; + + [[nodiscard]] std::vector parseFaces() const; + + [[nodiscard]] std::vector parseEdges() const; + + [[nodiscard]] std::vector parseSurfEdges() const; + + [[nodiscard]] std::vector parseBrushModels() const; + + [[nodiscard]] std::vector parseOriginalFaces() const; + + [[nodiscard]] std::vector parseGameLumps(bool decompress) const; + + [[nodiscard]] static std::optional> compressLumpData(const std::span data, uint8_t compressLevel = 6); + + [[nodiscard]] static std::optional> decompressLumpData(const std::span data); + + std::string path; + Header header{}; + + uint32_t stagedVersion{}; + std::unordered_map>> stagedLumps; + std::vector stagedGameLumps; + uint32_t stagedMapRevision{}; + + // Slightly different header despite using the same version just to be quirky + bool isL4D2 = false; +}; + +} // namespace bsppp diff --git a/include/bsppp/EntityLump.h b/include/bsppp/EntityLump.h new file mode 100644 index 000000000..65a5679b5 --- /dev/null +++ b/include/bsppp/EntityLump.h @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace bsppp { + +template +concept BSPEntityKeyValueType = std::convertible_to + || std::same_as + || std::same_as + || std::same_as + || std::same_as; + +class BSPEntityKeyValues { +public: + class Element { + friend class BSPEntityKeyValues; + + public: + /// Get the key associated with the element + [[nodiscard]] std::string_view getKey() const; + + /// Set the key associated with the element + void setKey(std::string_view key_); + + /// Get the value associated with the element + [[nodiscard]] std::string_view getValue() const; + + /// Get the value associated with the element as the given type + template + [[nodiscard]] V getValue() const { + if constexpr (std::convertible_to) { + return this->value; + } else if constexpr (std::same_as) { + return static_cast(this->getValue()); + } else if constexpr (std::same_as) { + return std::stoi(std::string{this->value}); + } else if constexpr (std::same_as) { + return std::stoll(std::string{this->value}); + } else if constexpr (std::same_as) { + return std::stof(std::string{this->value}); + } + return V{}; + } + + /// Set the value associated with the element + template + void setValue(V value_) { + if constexpr (std::convertible_to) { + this->value = std::string_view{value_}; + } else { + this->setValue(std::to_string(value_)); + } + } + + /// Set the value associated with the element + template + Element& operator=(V value_) { + this->setValue(value_); + return *this; + } + + /// Check if the given element is invalid + [[nodiscard]] bool isInvalid() const; + + protected: + Element() = default; + + static const Element& getInvalid(); + + std::string key; + std::string value; + }; + + BSPEntityKeyValues() = default; + + /// Check if this entity has one or more keyvalues with the given name + [[nodiscard]] bool hasChild(std::string_view childKey) const; + + /// Get the number of keyvalues + [[nodiscard]] uint64_t getKeyValuesCount() const; + + /// Get the number of keyvalues with the given key + [[nodiscard]] uint64_t getKeyValuesCount(std::string_view childKey) const; + + /// Get the keyvalues of the entity + [[nodiscard]] const std::vector& getKeyValues() const; + + /// Get the keyvalue of the entity at the given index + [[nodiscard]] const Element& operator[](unsigned int n) const; + + /// Get the keyvalue of the entity at the given index + [[nodiscard]] Element& operator[](unsigned int n); + + /// Get the first keyvalue of the entity with the given key + [[nodiscard]] const Element& operator[](std::string_view childKey) const; + + /// Get the first keyvalue of the entity with the given key, or create a new keyvalue if it doesn't exist + [[nodiscard]] Element& operator[](std::string_view childKey); + + /// Get the first keyvalue of the entity with the given key + [[nodiscard]] const Element& operator()(std::string_view childKey) const; + + /// Get the first keyvalue of the entity with the given key, or create a new keyvalue if it doesn't exist + [[nodiscard]] Element& operator()(std::string_view childKey); + + /// Get the nth keyvalue of the entity with the given key + [[nodiscard]] const Element& operator()(std::string_view childKey, unsigned int n) const; + + /// Get the nth keyvalue of the element with the given key, or create a new entity if it doesn't exist + [[nodiscard]] Element& operator()(std::string_view childKey, unsigned int n); + + template + Element& addKeyValue(std::string_view key_, V value_ = {}) { + Element elem; + elem.setKey(key_); + elem.setValue(value_); + this->keyvalues.push_back(elem); + return this->keyvalues.back(); + } + + [[nodiscard]] std::string bake(bool useEscapes) const; + +protected: + std::vector keyvalues; +}; + +} // namespace bsppp diff --git a/include/bsppp/LumpData.h b/include/bsppp/LumpData.h index 20af29078..7c4b6acfa 100644 --- a/include/bsppp/LumpData.h +++ b/include/bsppp/LumpData.h @@ -1,5 +1,8 @@ #pragma once +#include + +#include #include namespace bsppp { @@ -41,6 +44,56 @@ using BSPVertex = BSPVertex_v0; //endregion +//region Lump 5 (Nodes) + +struct BSPNode_v0 { + uint32_t planeNum; + int32_t children[2]; + int16_t mins[3]; + int16_t maxs[3]; + uint16_t firstFace; + uint16_t numFaces; + int16_t area; + uint16_t _unused0; +}; + +struct BSPNode_v1 { + uint32_t planeNum; + int32_t children[2]; + float mins[3]; + float maxs[3]; + uint32_t firstFace; + uint32_t numFaces; + int16_t area; + + [[nodiscard]] static BSPNode_v1 upgrade(const BSPNode_v0& old) { + return { + old.planeNum, + { + old.children[0], + old.children[1], + }, + { + static_cast(old.mins[0]), + static_cast(old.mins[1]), + static_cast(old.mins[2]), + }, + { + static_cast(old.maxs[0]), + static_cast(old.maxs[1]), + static_cast(old.maxs[2]), + }, + old.firstFace, + old.numFaces, + old.area, + }; + } +}; + +using BSPNode = BSPNode_v1; + +//endregion + //region Lump 6 (Texture Info) struct BSPTextureInfo_v0 { @@ -177,4 +230,25 @@ using BSPBrushModel = BSPBrushModel_v0; //endregion +//region Lump 35 (Game Lump) + +struct BSPGameLump { + enum Signature : uint32_t { + SIGNATURE_STATIC_PROPS = sourcepp::parser::binary::makeFourCC("sprp"), + SIGNATURE_DETAIL_PROPS = sourcepp::parser::binary::makeFourCC("dprp"), + SIGNATURE_DETAIL_PROP_LIGHTING_LDR = sourcepp::parser::binary::makeFourCC("dplt"), + SIGNATURE_DETAIL_PROP_LIGHTING_HDR = sourcepp::parser::binary::makeFourCC("dplh"), + } signature; + uint16_t isCompressed; + uint16_t version; + // Offset from beginning of file, not game lump (except Portal 2 on console but we'll ignore that for now) + uint32_t offset; + // Compressed size is determined by subtracting the next entry's offset with this one like VTF resources + uint32_t uncompressedLength; + + std::vector data; +}; + +//endregion + } // namespace bsppp diff --git a/include/bsppp/bsppp.h b/include/bsppp/bsppp.h index 4d097c5a9..05a44b8bf 100644 --- a/include/bsppp/bsppp.h +++ b/include/bsppp/bsppp.h @@ -1,211 +1,10 @@ #pragma once -#include -#include -#include -#include -#include -#include - -#include +/* + * This header is just included so consumers of this library can + * include it the same way as any of the other SourcePP libraries. + */ +#include "BSP.h" #include "LumpData.h" #include "PakLump.h" - -namespace bsppp { - -constexpr auto BSP_SIGNATURE = sourcepp::parser::binary::makeFourCC("VBSP"); - -enum class BSPLump : int32_t { - UNKNOWN = -1, - ENTITIES = 0, - PLANES, - TEXDATA, - VERTEXES, - VISIBILITY, - NODES, - TEXINFO, - FACES, - LIGHTING, - OCCLUSION, - LEAFS, - FACEIDS, - EDGES, - SURFEDGES, - MODELS, - WORLDLIGHTS, - LEAFFACES, - LEAFBRUSHES, - BRUSHES, - BRUSHSIDES, - AREAS, - AREAPORTALS, - S2004_PORTALS, - UNUSED0 = S2004_PORTALS, - SL4D2_PROPCOLLISION = S2004_PORTALS, - S2004_CLUSTERS, - UNUSED1 = S2004_CLUSTERS, - SL4D2_PROPHULLS = S2004_CLUSTERS, - S2004_PORTALVERTS, - UNUSED2 = S2004_PORTALVERTS, - SL4D2_PROPHULLVERTS = S2004_PORTALVERTS, - S2004_CLUSTERPORTALS, - UNUSED3 = S2004_CLUSTERPORTALS, - SL4D2_PROPTRIS = S2004_CLUSTERPORTALS, - DISPINFO, - ORIGINALFACES, - PHYSDISP, - PHYSCOLLIDE, - VERTNORMALS, - VERTNORMALINDICES, - S2004_DISP_LIGHTMAP_ALPHAS, - UNUSED4 = S2004_DISP_LIGHTMAP_ALPHAS, - DISP_VERTS, - DISP_LIGHTMAP_SAMPLE_POSITIONS, - GAME_LUMP, - LEAFWATERDATA, - PRIMITIVES, - PRIMVERTS, - PRIMINDICES, - PAKFILE, - CLIPPORTALVERTS, - CUBEMAPS, - TEXDATA_STRING_DATA, - TEXDATA_STRING_TABLE, - OVERLAYS, - LEAFMINDISTTOWATER, - FACE_MACRO_TEXTURE_INFO, - DISP_TRIS, - S2004_PHYSCOLLIDESURFACE, - UNUSED5 = S2004_PHYSCOLLIDESURFACE, - SL4D2_PROP_BLOB = S2004_PHYSCOLLIDESURFACE, - WATEROVERLAYS, - S2006_XBOX_LIGHTMAPPAGES, - LEAF_AMBIENT_INDEX_HDR = S2006_XBOX_LIGHTMAPPAGES, - S2006_XBOX_LIGHTMAPPAGEINFOS, - LEAF_AMBIENT_INDEX = S2006_XBOX_LIGHTMAPPAGEINFOS, - LIGHTING_HDR, - WORLDLIGHTS_HDR, - LEAF_AMBIENT_LIGHTING_HDR, - LEAF_AMBIENT_LIGHTING, - XBOX_XZIPPAKFILE, - FACES_HDR, - MAP_FLAGS, - OVERLAY_FADES, - L4D_OVERLAY_SYSTEM_LEVELS, - UNUSED6 = L4D_OVERLAY_SYSTEM_LEVELS, - L4D2_PHYSLEVEL, - UNUSED7 = L4D2_PHYSLEVEL, - ASW_DISP_MULTIBLEND, - UNUSED8 = ASW_DISP_MULTIBLEND, - - COUNT, -}; -static_assert(static_cast(BSPLump::COUNT) == 64, "Incorrect lump count!"); - -constexpr auto BSP_LUMP_COUNT = static_cast(BSPLump::COUNT); - -class BSP { - struct Lump { - /// Byte offset into file - int32_t offset; - /// Length of lump data - int32_t length; - /// Lump format version - int32_t version; - /// Uncompressed size, or 0 - int32_t fourCC; - }; - - struct Header { - /// Version of the BSP file - int32_t version; - /// Lump metadata - std::array lumps; - /// Map version number - int32_t mapRevision; - }; - -public: - explicit BSP(std::string path_); - - explicit operator bool() const; - - static BSP create(std::string path, int32_t version = 21, int32_t mapRevision = 0); - - [[nodiscard]] int32_t getVersion() const; - - void setVersion(int32_t version); - - [[nodiscard]] int32_t getMapRevision() const; - - void setMapRevision(int32_t mapRevision); - - [[nodiscard]] bool hasLump(BSPLump lumpIndex) const; - - [[nodiscard]] int32_t getLumpVersion(BSPLump lumpIndex) const; - - void setLumpVersion(BSPLump lumpIndex, int32_t version); - - [[nodiscard]] std::optional> readLump(BSPLump lumpIndex) const; - - template - [[nodiscard]] auto readLump() const { - if constexpr (Lump == BSPLump::PLANES) { - return this->readPlanes(); - } else if constexpr (Lump == BSPLump::TEXDATA) { - return this->readTextureData(); - } else if constexpr (Lump == BSPLump::VERTEXES) { - return this->readVertices(); - } else if constexpr (Lump == BSPLump::TEXINFO) { - return this->readTextureInfo(); - } else if constexpr (Lump == BSPLump::FACES) { - return this->readFaces(); - } else if constexpr (Lump == BSPLump::EDGES) { - return this->readEdges(); - } else if constexpr (Lump == BSPLump::SURFEDGES) { - return this->readSurfEdges(); - } else if constexpr (Lump == BSPLump::MODELS) { - return this->readBrushModels(); - } else if constexpr (Lump == BSPLump::ORIGINALFACES) { - return this->readOriginalFaces(); - } else { - return this->readLump(Lump); - } - } - - void writeLump(BSPLump lumpIndex, std::span data, bool condenseFile = true); - - bool applyLumpPatchFile(const std::string& lumpFilePath); - - void createLumpPatchFile(BSPLump lumpIndex) const; - -protected: - void writeHeader() const; - - [[nodiscard]] std::vector readPlanes() const; - - [[nodiscard]] std::vector readTextureData() const; - - [[nodiscard]] std::vector readVertices() const; - - [[nodiscard]] std::vector readTextureInfo() const; - - [[nodiscard]] std::vector readFaces() const; - - [[nodiscard]] std::vector readEdges() const; - - [[nodiscard]] std::vector readSurfEdges() const; - - [[nodiscard]] std::vector readBrushModels() const; - - [[nodiscard]] std::vector readOriginalFaces() const; - - std::string path; - Header header{}; - - // Slightly different header just to be quirky and special - bool isL4D2 = false; -}; - -} // namespace bsppp diff --git a/include/kvpp/KV1.h b/include/kvpp/KV1.h index 378bd45fe..3238d2457 100644 --- a/include/kvpp/KV1.h +++ b/include/kvpp/KV1.h @@ -15,7 +15,8 @@ namespace kvpp { template concept KV1ValueType = std::convertible_to || std::same_as - || std::same_as + || std::same_as + || std::same_as || std::same_as; template @@ -38,9 +39,17 @@ class KV1ElementBase { if constexpr (std::convertible_to) { return this->value; } else if constexpr (std::same_as) { - return static_cast(this->getValue()); - } else if constexpr (std::same_as) { + return static_cast(this->getValue()); + } else if constexpr (std::same_as) { + if (this->value.length() == 10 && this->value.starts_with("0x") && sourcepp::parser::text::isNumber(this->value.substr(2))) { + return std::stoi(std::string{this->value.substr(2)}, nullptr, 16); + } return std::stoi(std::string{this->value}); + } else if constexpr (std::same_as) { + if (this->value.length() == 18 && this->value.starts_with("0x") && sourcepp::parser::text::isNumber(this->value.substr(2))) { + return std::stoll(std::string{this->value.substr(2)}, nullptr, 16); + } + return std::stoll(std::string{this->value}); } else if constexpr (std::same_as) { return std::stof(std::string{this->value}); } @@ -206,7 +215,7 @@ class KV1ElementWritable : public KV1ElementBase> { void setValue(V value_) { if constexpr (std::convertible_to) { this->value = std::string_view{value_}; - } else if constexpr (std::same_as || std::same_as || std::same_as) { + } else { this->setValue(std::to_string(value_)); } } diff --git a/include/sourcepp/parser/Text.h b/include/sourcepp/parser/Text.h index 72694a590..84f14e5ee 100644 --- a/include/sourcepp/parser/Text.h +++ b/include/sourcepp/parser/Text.h @@ -136,7 +136,7 @@ void eatWhitespaceAndComments(BufferStream& stream, std::string_view singleLineC * @param escapeSequences Characters that will be escaped if a backslash is present before them. To disable escapes, pass an empty map. * @return A view over the string written to the backing stream. */ -[[nodiscard]] 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); +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. @@ -145,7 +145,7 @@ void eatWhitespaceAndComments(BufferStream& stream, std::string_view singleLineC * @param escapeSequences Characters that will be escaped if a backslash is present before them. To disable escapes, pass an empty map. * @return A view over the string written to the backing stream. */ -[[nodiscard]] std::string_view readUnquotedStringToBuffer(BufferStream& stream, BufferStream& backing, const EscapeSequenceMap& escapeSequences = DEFAULT_ESCAPE_SEQUENCES); +std::string_view readUnquotedStringToBuffer(BufferStream& stream, BufferStream& backing, const EscapeSequenceMap& escapeSequences = DEFAULT_ESCAPE_SEQUENCES); /** * Read a string starting at the current stream position. @@ -155,13 +155,11 @@ void eatWhitespaceAndComments(BufferStream& stream, std::string_view singleLineC * @param escapeSequences Characters that will be escaped if a backslash is present before them. To disable escapes, pass an empty map. * @return A view over the string written to the backing stream. */ -[[nodiscard]] std::string_view readUnquotedStringToBuffer(BufferStream& stream, BufferStream& backing, std::string_view end, const EscapeSequenceMap& escapeSequences = DEFAULT_ESCAPE_SEQUENCES); +std::string_view readUnquotedStringToBuffer(BufferStream& stream, BufferStream& backing, std::string_view end, const EscapeSequenceMap& escapeSequences = DEFAULT_ESCAPE_SEQUENCES); class syntax_error : public std::runtime_error { public: - explicit syntax_error(const char* message) noexcept : std::runtime_error(message) {} - syntax_error(const syntax_error& other) noexcept = default; - syntax_error& operator=(const syntax_error& other) noexcept = default; + using std::runtime_error::runtime_error; }; } // namespace sourcepp::parser::text diff --git a/src/bsppp/BSP.cpp b/src/bsppp/BSP.cpp new file mode 100644 index 000000000..1846760d7 --- /dev/null +++ b/src/bsppp/BSP.cpp @@ -0,0 +1,830 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include + +using namespace bsppp; +using namespace sourcepp; + +namespace { + +template +[[nodiscard]] std::vector parseLumpContents(const BSP& bsp, BSPLump lump, void(*callback)(const BSP&, BufferStreamReadOnly&, std::vector&) = [](const BSP&, BufferStreamReadOnly& stream, std::vector& out) { + stream.read(out, stream.size() / sizeof(T)); +}) { + auto data = bsp.getLumpData(lump); + if (!data) { + return {}; + } + + BufferStreamReadOnly stream{*data}; + std::vector out; + callback(bsp, stream, out); + return out; +} + +template +requires requires(New) { {New::upgrade(Old{})} -> std::same_as; } +void parseAndUpgrade(BufferStreamReadOnly& stream, std::vector& out) { + std::vector old; + stream.read(old, stream.size() / sizeof(Old)); + for (const auto& elem : old) { + out.push_back(New::upgrade(elem)); + } +} + +} // namespace + +BSP::BSP(std::string path_, bool loadPatchFiles) + : path(std::move(path_)) { + if (!this->readHeader()) { + this->path.clear(); + return; + } + + this->stagedVersion = this->header.version; + this->stagedGameLumps = this->parseGameLumps(false); + this->stagedMapRevision = this->header.mapRevision; + + if (loadPatchFiles) { + const auto fsPath = std::filesystem::path{this->path}; + const auto fsStem = (fsPath.parent_path() / fsPath.stem()).string() + "_l_"; + + for (int i = 0; ; i++) { + auto patchFilePath = fsStem + std::to_string(i) + ".lmp"; + if (!std::filesystem::exists(patchFilePath)) { + break; + } + this->setLumpFromPatchFile(patchFilePath); + } + } +} + +BSP::operator bool() const { + return !this->path.empty(); +} + +BSP BSP::create(std::string path, uint32_t version, uint32_t mapRevision) { + { + FileStream writer{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT}; + writer << BSP_SIGNATURE << version; + if (version == 27) { + writer.write(0); + } + for (int i = 0; i < BSP_LUMP_COUNT; i++) { + writer + .write(0) + .write(0) + .write(0) + .write(0); + } + writer << mapRevision; + } + return BSP{std::move(path)}; +} + +uint32_t BSP::getVersion() const { + return this->stagedVersion; +} + +void BSP::setVersion(uint32_t version) { + this->stagedVersion = version; +} + +uint32_t BSP::getMapRevision() const { + return this->stagedMapRevision; +} + +void BSP::setMapRevision(uint32_t mapRevision) { + this->stagedMapRevision = mapRevision; +} + +bool BSP::hasLump(BSPLump lumpIndex) const { + if (this->path.empty()) { + return false; + } + const auto lump = static_cast>(lumpIndex); + return (this->stagedLumps.contains(lump) && this->stagedLumps.at(lump).first.length != 0 && this->stagedLumps.at(lump).first.offset != 0) + || (this->header.lumps[lump].length != 0 && this->header.lumps[lump].offset != 0); +} + +bool BSP::isLumpCompressed(BSPLump lumpIndex) const { + if (this->hasLump(lumpIndex)) { + const auto lump = static_cast>(lumpIndex); + return (this->stagedLumps.contains(lump) && this->stagedLumps.at(lump).first.uncompressedLength > 0) + || (this->header.lumps[lump].uncompressedLength > 0); + } + return false; +} + +uint32_t BSP::getLumpVersion(BSPLump lumpIndex) const { + if (this->path.empty()) { + return 0; + } + const auto lump = static_cast>(lumpIndex); + if (this->stagedLumps.contains(lump)) { + return this->stagedLumps.at(lump).first.version; + } + return this->header.lumps[lump].version; +} + +std::optional> BSP::getLumpData(BSPLump lumpIndex, bool noDecompression) const { + if (this->path.empty() || !this->hasLump(lumpIndex)) { + return std::nullopt; + } + + auto lump = static_cast>(lumpIndex); + std::vector lumpBytes; + + if (this->stagedLumps.contains(lump)) { + lumpBytes = this->stagedLumps.at(lump).second; + } else { + FileStream reader{this->path}; + lumpBytes = reader + .seek_in(this->header.lumps[lump].offset) + .read_bytes(this->header.lumps[lump].length); + } + + if (!noDecompression && this->isLumpCompressed(lumpIndex)) { + return decompressLumpData(lumpBytes); + } + return lumpBytes; +} + +bool BSP::setLump(BSPLump lumpIndex, uint32_t version, std::span data, uint8_t compressLevel) { + if (this->path.empty() || lumpIndex == BSPLump::UNKNOWN) { + return false; + } + if (lumpIndex == BSPLump::GAME_LUMP) { + // Game lump needs to be modified with other methods + return false; + } + + if (lumpIndex == BSPLump::PAKFILE) { + // Paklump should use zip compression + compressLevel = 0; + } + + const auto lump = static_cast>(lumpIndex); + if (compressLevel > 0) { + auto compressedData = compressLumpData(data, compressLevel); + if (!compressedData) { + return false; + } + this->stagedLumps[lump] = std::make_pair>({ + .version = version, .uncompressedLength = static_cast(data.size()), + }, {compressedData->begin(), compressedData->end()}); + } else { + this->stagedLumps[lump] = std::make_pair>({ + .version = version, .uncompressedLength = 0, + }, {data.begin(), data.end()}); + } + return true; +} + +bool BSP::setLump(uint32_t version, std::span data, uint8_t compressLevel) { + if (version > 1) { + return false; + } + std::string out; + for (const auto& ent : data) { + out += ent.bake(version == 1) + '\n'; + } + if (out.empty()) { + out += '\0'; + } else { + out.back() = '\0'; + } + return this->setLump(BSPLump::ENTITIES, version, {reinterpret_cast(out.data()), out.size()}, compressLevel); +} + +bool BSP::isGameLumpCompressed(BSPGameLump::Signature signature) const { + for (const auto& gameLump : this->stagedGameLumps) { + if (gameLump.signature == signature) { + return gameLump.isCompressed; + } + } + return false; +} + +uint16_t BSP::getGameLumpVersion(BSPGameLump::Signature signature) { + for (const auto& gameLump : this->stagedGameLumps) { + if (gameLump.signature == signature) { + return gameLump.version; + } + } + return 0; +} + +std::optional> BSP::getGameLumpData(BSPGameLump::Signature signature) const { + for (const auto& gameLump : this->stagedGameLumps) { + if (gameLump.signature == signature) { + if (gameLump.isCompressed) { + return decompressLumpData(gameLump.data); + } + return gameLump.data; + } + } + return std::nullopt; +} + +bool BSP::setGameLump(BSPGameLump::Signature signature, uint16_t version, std::span data, uint8_t compressLevel) { + BSPGameLump gameLump{ + .signature = signature, + .isCompressed = compressLevel > 0, + .version = version, + .offset = 0, + .uncompressedLength = static_cast(data.size()), + }; + + if (compressLevel) { + const auto compressedData = compressLumpData(data, compressLevel); + if (!compressedData) { + return false; + } + gameLump.data = *compressedData; + } else { + gameLump.data = {data.begin(), data.end()}; + } + + this->stagedGameLumps.push_back(gameLump); + return true; +} + +void BSP::resetLump(BSPLump lumpIndex) { + if (this->path.empty()) { + return; + } + if (lumpIndex == BSPLump::UNKNOWN) { + this->reset(); + return; + } + if (lumpIndex == BSPLump::GAME_LUMP) { + this->stagedGameLumps = this->parseGameLumps(false); + return; + } + const auto lump = static_cast>(lumpIndex); + if (this->stagedLumps.contains(lump)) { + this->stagedLumps.erase(lump); + } +} + +void BSP::reset() { + if (this->path.empty()) { + return; + } + this->stagedVersion = this->header.version; + this->stagedLumps.clear(); + this->stagedGameLumps = this->parseGameLumps(false); + this->stagedMapRevision = this->header.mapRevision; +} + +void BSP::createLumpPatchFile(BSPLump lumpIndex) const { + if (this->path.empty()) { + return; + } + + auto lumpData = this->getLumpData(lumpIndex); + if (!lumpData) { + return; + } + + const auto& [ + lumpOffset, + lumpLength, + lumpVersion, + lumpUncompressedLength + ] = this->header.lumps.at(static_cast>(lumpIndex)); + + const auto fsPath = std::filesystem::path{this->path}; + const auto fsStem = (fsPath.parent_path() / fsPath.stem()).string() + "_l_"; + int nonexistentNumber = 0; + while (true) { + if (!std::filesystem::exists(fsStem + std::to_string(nonexistentNumber) + ".lmp")) { + break; + } + nonexistentNumber++; + } + + FileStream writer{fsStem + std::to_string(nonexistentNumber) + ".lmp", FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT}; + writer + .seek_out(0) + .write(sizeof(int32_t) * 5) + .write(lumpIndex) + .write(lumpVersion) + .write(lumpLength) + .write(this->header.mapRevision) + .write(*lumpData); +} + +bool BSP::setLumpFromPatchFile(const std::string& lumpFilePath) { + if (this->path.empty()) { + return false; + } + + FileStream reader{lumpFilePath}; + if (!reader) { + return false; + } + + const auto offset = reader.read(); + const auto index = reader.read(); + const auto version = reader.read(); + const auto length = reader.read(); + if (index > BSP_LUMP_COUNT || offset == 0 || length == 0) { + return false; + } + + this->setLump(static_cast(index), version, reader.seek_in(offset).read_bytes(length)); + return true; +} + +bool BSP::bake(std::string_view outputPath) { + if (this->path.empty()) { + return false; + } + + std::vector out; + BufferStream stream{out}; + + stream << BSP_SIGNATURE << this->stagedVersion; + if (this->stagedVersion == 27) { + // Contagion funny + stream.write(0); + } + + const auto lumpsHeaderOffset = stream.tell(); + for (int i = 0; i < sizeof(Header::lumps); i++) { + stream.write(0); + } + + stream << this->stagedMapRevision; + + for (auto i : BSP_LUMP_ORDER) { + if (!this->hasLump(static_cast(i))) { + continue; + } + + // Lumps are 4 byte aligned + if (const auto padding = math::paddingForAlignment(4, stream.tell()); padding > 0) { + for (int p = 0; p < padding; p++) { + stream.write(0); + } + } + + if (static_cast(i) == BSPLump::GAME_LUMP && !this->stagedGameLumps.empty()) { + const auto gameLumpOffset = stream.tell(); + + bool oneOrMoreGameLumpCompressed = false; + for (const auto& gameLump : this->stagedGameLumps) { + if (gameLump.isCompressed) { + oneOrMoreGameLumpCompressed = true; + break; + } + } + // NOLINTNEXTLINE(*-sizeof-container) + auto gameLumpCurrentOffset = stream.tell() + sizeof(int32_t) + ((sizeof(BSPGameLump) - sizeof(BSPGameLump::data)) * (this->stagedGameLumps.size() + oneOrMoreGameLumpCompressed)); + stream.write(this->stagedGameLumps.size() + oneOrMoreGameLumpCompressed); + + for (const auto& gameLump : this->stagedGameLumps) { + if (gameLump.signature == 0) { + break; + } + stream + .write(gameLump.signature) + .write(gameLump.isCompressed) + .write(gameLump.version) + .write(gameLumpCurrentOffset) + .write(gameLump.uncompressedLength); + gameLumpCurrentOffset += gameLump.data.size(); + } + if (oneOrMoreGameLumpCompressed) { + stream + .write(0) + .write(0) + .write(0) + .write(0) + .write(0); + } + + for (const auto& gameLump : this->stagedGameLumps) { + if (gameLump.signature == 0) { + break; + } + stream.write(gameLump.data); + } + + const auto curPos = stream.tell(); + stream.seek_u(lumpsHeaderOffset + (i * sizeof(Lump))); + if (!this->isL4D2) { + stream + .write(gameLumpOffset) + .write(curPos - gameLumpOffset) + .write(0); + } else { + stream + .write(0) + .write(gameLumpOffset) + .write(curPos - gameLumpOffset); + } + stream + .write(0) + .seek_u(curPos); + continue; + } + + if (this->stagedLumps.contains(i)) { + const auto& lumpPair = this->stagedLumps.at(i); + const auto curPos = stream.tell(); + stream.seek_u(lumpsHeaderOffset + (i * sizeof(Lump))); + if (!this->isL4D2) { + stream + .write(curPos) + .write(lumpPair.second.size()) + .write(lumpPair.first.version); + } else { + stream + .write(lumpPair.first.version) + .write(curPos) + .write(lumpPair.second.size()); + } + stream + .write(lumpPair.first.uncompressedLength) + .seek_u(curPos) + .write(lumpPair.second); + continue; + } + + const auto data = this->getLumpData(static_cast(i), true); + if (data) { + const auto curPos = stream.tell(); + stream.seek_u(lumpsHeaderOffset + (i * sizeof(Lump))); + + auto& lump = this->header.lumps[i]; + if (!this->isL4D2) { + stream + .write(curPos) + .write(lump.length) + .write(lump.version); + } else { + stream + .write(lump.version) + .write(curPos) + .write(lump.length); + } + stream + .write(lump.uncompressedLength) + .seek_u(curPos) + .write(*data); + } else { + // We should never be here! + SOURCEPP_DEBUG_BREAK; + } + } + + // Lumps are 4 byte aligned + if (const auto padding = math::paddingForAlignment(4, stream.tell()); padding > 0) { + for (int p = 0; p < padding; p++) { + stream.write(0); + } + } + + out.resize(stream.tell()); + if (!fs::writeFileBuffer(outputPath.empty() ? this->path : std::string{outputPath}, out)) { + return false; + } + + this->path = outputPath; + this->stagedLumps.clear(); + return this->readHeader(); +} + +bool BSP::readHeader() { + FileStream reader{this->path}; + if (!reader) { + return false; + } + reader.seek_in(0); + + if (reader.read() != BSP_SIGNATURE) { + // File is not a BSP + return false; + } + this->header.version = reader.read(); + + // Contagion funny + if (this->header.version == 27) { + reader.skip_in(); + } + + for (auto& lump : this->header.lumps) { + reader + >> lump.offset + >> lump.length + >> lump.version + >> lump.uncompressedLength; + } + + // If no offsets are larger than 1024 (less than the size of the BSP header, but greater + // than any lump version), it's probably a L4D2 BSP and those are lump versions! + if (this->header.version == 21) { + int i; + for (i = 0; i < BSP_LUMP_COUNT; i++) { + if (this->header.lumps[i].offset > 1024) { + break; + } + } + this->isL4D2 = i == BSP_LUMP_COUNT; + if (this->isL4D2) { + // Swap fields around + for (i = 0; i < BSP_LUMP_COUNT; i++) { + std::swap(this->header.lumps[i].offset, this->header.lumps[i].version); + std::swap(this->header.lumps[i].offset, this->header.lumps[i].length); + } + } + } + + reader >> this->header.mapRevision; + return true; +} + +std::vector BSP::parseEntities() const { + using namespace sourcepp; + + const auto useEscapes = this->getLumpVersion(BSPLump::ENTITIES); + if (useEscapes > 1) { + return {}; + } + + auto data = this->getLumpData(BSPLump::ENTITIES); + if (!data) { + return {}; + } + BufferStreamReadOnly stream{*data}; + + std::vector entities; + + try { + while (true) { + // Check for EOF - give 3 chars for extra breathing room + if (stream.tell() >= stream.size() - 3) { + return entities; + } + + // Expect an opening brace + parser::text::eatWhitespaceAndSingleLineComments(stream); + if (stream.peek() != '{') { + break; + } + stream.skip(); + + auto& ent = entities.emplace_back(); + + while (true) { + // Check if the block is over + parser::text::eatWhitespaceAndSingleLineComments(stream); + if (stream.peek() == '}') { + stream.skip(); + break; + } + + std::string key, value; + + // Read key + { + BufferStream keyStream{key}; + parser::text::readStringToBuffer(stream, keyStream, parser::text::DEFAULT_STRING_START, parser::text::DEFAULT_STRING_END, useEscapes ? parser::text::DEFAULT_ESCAPE_SEQUENCES : parser::text::NO_ESCAPE_SEQUENCES); + key.resize(keyStream.tell() - 1); + parser::text::eatWhitespaceAndSingleLineComments(stream); + } + + // Read value + { + BufferStream valueStream{value}; + parser::text::readStringToBuffer(stream, valueStream, parser::text::DEFAULT_STRING_START, parser::text::DEFAULT_STRING_END, useEscapes ? parser::text::DEFAULT_ESCAPE_SEQUENCES : parser::text::NO_ESCAPE_SEQUENCES); + value.resize(valueStream.tell() - 1); + parser::text::eatWhitespaceAndSingleLineComments(stream); + } + + ent[key] = value; + } + } + } catch (const std::overflow_error&) { + return {}; + } + return entities; +} + +std::vector BSP::parsePlanes() const { + return ::parseLumpContents(*this, BSPLump::PLANES); +} + +std::vector BSP::parseTextureData() const { + return ::parseLumpContents(*this, BSPLump::TEXDATA); +} + +std::vector BSP::parseVertices() const { + return ::parseLumpContents(*this, BSPLump::VERTEXES); +} + +std::vector BSP::parseNodes() const { + return ::parseLumpContents(*this, BSPLump::NODES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector& out) { + if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::NODES); lumpVersion == 1) { + stream.read(out, stream.size() / sizeof(BSPNode_v1)); + } else if (lumpVersion == 0) { + ::parseAndUpgrade(stream, out); + } + }); +} + +std::vector BSP::parseTextureInfo() const { + return ::parseLumpContents(*this, BSPLump::TEXINFO); +} + +std::vector BSP::parseFaces() const { + return ::parseLumpContents(*this, BSPLump::FACES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector& out) { + if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::FACES); lumpVersion == 2) { + stream.read(out, stream.size() / sizeof(BSPFace_v2)); + } else if (lumpVersion == 1) { + ::parseAndUpgrade(stream, out); + } + }); +} + +std::vector BSP::parseEdges() const { + return ::parseLumpContents(*this, BSPLump::EDGES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector& out) { + if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::EDGES); lumpVersion == 1) { + stream.read(out, stream.size() / sizeof(BSPEdge_v1)); + } else if (lumpVersion == 0) { + ::parseAndUpgrade(stream, out); + } + }); +} + +std::vector BSP::parseSurfEdges() const { + return ::parseLumpContents(*this, BSPLump::SURFEDGES); +} + +std::vector BSP::parseBrushModels() const { + return ::parseLumpContents(*this, BSPLump::MODELS); +} + +std::vector BSP::parseOriginalFaces() const { + return ::parseLumpContents(*this, BSPLump::ORIGINALFACES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector& out) { + // ORIGINALFACES lump version is always 0? + if (const auto lumpVersion = bsp.getLumpVersion(BSPLump::FACES); lumpVersion == 2) { + stream.read(out, stream.size() / sizeof(BSPFace_v2)); + } else if (lumpVersion == 1) { + ::parseAndUpgrade(stream, out); + } + }); +} + +std::vector BSP::parseGameLumps(bool decompress) const { + std::vector lumps; + + auto gameLumpData = this->getLumpData(BSPLump::GAME_LUMP); + if (!gameLumpData) { + return lumps; + } + BufferStreamReadOnly stream{*gameLumpData}; + + lumps.resize(stream.read()); + for (auto& lump : lumps) { + stream + .read(lump.signature) + .read(lump.isCompressed) + .read(lump.version) + .read(lump.offset) + .read(lump.uncompressedLength); + } + + // When one or more game lumps are compressed, the last entry + // is empty except the offset field to calculate compressed + // size. Game lumps are compressed individually + for (uint32_t i = 0; i < lumps.size(); i++) { + if (lumps[i].signature == 0) { + break; + } + if (!lumps[i].isCompressed) { + lumps[i].data = stream.read_bytes(lumps[i].uncompressedLength); + } else { + auto nextOffset = lumps[i + 1].offset; + if (nextOffset == 0) { + const auto id = static_cast>(BSPLump::GAME_LUMP); + nextOffset = this->header.lumps[id].offset + this->header.lumps[id].length; + } + if (!decompress) { + lumps[i].data = stream.read_bytes(nextOffset - lumps[i].offset); + } else { + auto uncompressedData = decompressLumpData(stream.read_bytes(nextOffset - lumps[i].offset)); + if (uncompressedData) { + lumps[i].data = *uncompressedData; + } + } + } + } + + if (lumps.back().signature == 0) { + lumps.pop_back(); + } + return lumps; +} + +std::optional> BSP::compressLumpData(std::span data, uint8_t compressLevel) { + // Preallocate extra 4 bytes for Valve LZMA header signature + std::vector compressedData(sizeof(uint32_t)); + std::array compressedChunk{}; + + lzma_stream stream{ + .next_in = reinterpret_cast(data.data()), + .avail_in = data.size(), + .next_out = reinterpret_cast(compressedChunk.data()), + .avail_out = compressedChunk.size(), + }; + + lzma_options_lzma options{}; + lzma_lzma_preset(&options, std::clamp(compressLevel, 0, 9)); + + if (lzma_alone_encoder(&stream, &options) != LZMA_OK) { + lzma_end(&stream); + return std::nullopt; + } + + lzma_ret ret; + do { + stream.next_out = reinterpret_cast(compressedChunk.data()); + stream.avail_out = compressedChunk.size(); + + ret = lzma_code(&stream, LZMA_RUN); + compressedData.insert(compressedData.end(), compressedChunk.begin(), compressedChunk.begin() + compressedChunk.size() - stream.avail_out); + } while (ret == LZMA_OK); + + if (auto code = lzma_code(&stream, LZMA_FINISH); code != LZMA_OK && code != LZMA_STREAM_END) { + lzma_end(&stream); + return std::nullopt; + } + lzma_end(&stream); + + { + // Switch out normal header with Valve one + BufferStream compressedStream{compressedData}; + compressedStream << LZMA_VALVE_SIGNATURE; + const auto properties = compressedStream.read>(); + compressedStream + .seek_u(sizeof(uint32_t)) + .write(data.size()) + .write(compressedData.size() - (sizeof(uint32_t) * 3) + (sizeof(uint8_t) * 5)) + .write(properties); + } + return compressedData; +} + +std::optional> BSP::decompressLumpData(std::span data) { + std::vector compressedData{data.begin(), data.end()}; + std::vector uncompressedData; + { + // Switch out Valve header with normal one + BufferStreamReadOnly in{data.data(), data.size()}; + if (in.read() != LZMA_VALVE_SIGNATURE) { + return std::nullopt; + } + const auto uncompressedLength = in.read(); + in.skip(); + const auto properties = in.read>(); + BufferStream out{compressedData}; + out + .write(properties) + .write(uncompressedLength); + uncompressedData.resize(uncompressedLength); + } + + lzma_stream stream{ + .next_in = reinterpret_cast(compressedData.data()), + .avail_in = compressedData.size(), + .next_out = reinterpret_cast(uncompressedData.data()), + .avail_out = uncompressedData.size(), + }; + + if (lzma_alone_decoder(&stream, UINT64_MAX) != LZMA_OK) { + lzma_end(&stream); + return std::nullopt; + } + if (auto ret = lzma_code(&stream, LZMA_RUN); ret != LZMA_OK && ret != LZMA_STREAM_END) { + lzma_end(&stream); + return std::nullopt; + } + if (auto ret = lzma_code(&stream, LZMA_FINISH); ret != LZMA_OK && ret != LZMA_STREAM_END) { + lzma_end(&stream); + return std::nullopt; + } + lzma_end(&stream); + + return uncompressedData; +} diff --git a/src/bsppp/EntityLump.cpp b/src/bsppp/EntityLump.cpp new file mode 100644 index 000000000..829e2460e --- /dev/null +++ b/src/bsppp/EntityLump.cpp @@ -0,0 +1,120 @@ +#include + +using namespace bsppp; +using namespace sourcepp; + +std::string_view BSPEntityKeyValues::Element::getKey() const { + return this->key; +} + +void BSPEntityKeyValues::Element::setKey(std::string_view key_) { + this->key = key_; +} + +std::string_view BSPEntityKeyValues::Element::getValue() const { + return this->value; +} + +bool BSPEntityKeyValues::Element::isInvalid() const { + return this == &getInvalid(); +} + +const BSPEntityKeyValues::Element& BSPEntityKeyValues::Element::getInvalid() { + static Element element; + return element; +} + +bool BSPEntityKeyValues::hasChild(std::string_view childKey) const { + return !this->operator[](childKey).isInvalid(); +} + +uint64_t BSPEntityKeyValues::getKeyValuesCount() const { + return this->keyvalues.size(); +} + +uint64_t BSPEntityKeyValues::getKeyValuesCount(std::string_view childKey) const { + uint64_t count = 0; + for (const auto& element : this->keyvalues) { + if (sourcepp::string::iequals(element.getKey(), childKey)) { + ++count; + } + } + return count; +} + +const std::vector& BSPEntityKeyValues::getKeyValues() const { + return this->keyvalues; +} + +const BSPEntityKeyValues::Element& BSPEntityKeyValues::operator[](unsigned int n) const { + return this->keyvalues.at(n); +} + +BSPEntityKeyValues::Element& BSPEntityKeyValues::operator[](unsigned int n) { + return this->keyvalues.at(n); +} + +const BSPEntityKeyValues::Element& BSPEntityKeyValues::operator[](std::string_view childKey) const { + return this->operator()(childKey); +} + +BSPEntityKeyValues::Element& BSPEntityKeyValues::operator[](std::string_view childKey) { + return this->operator()(childKey); +} + +const BSPEntityKeyValues::Element& BSPEntityKeyValues::operator()(std::string_view childKey) const { + for (const auto& element : this->keyvalues) { + if (sourcepp::string::iequals(element.getKey(), childKey)) { + return element; + } + } + return Element::getInvalid(); +} + +BSPEntityKeyValues::Element& BSPEntityKeyValues::operator()(std::string_view childKey) { + for (auto& element : this->keyvalues) { + if (sourcepp::string::iequals(element.getKey(), childKey)) { + return element; + } + } + return this->addKeyValue(childKey); +} + +const BSPEntityKeyValues::Element& BSPEntityKeyValues::operator()(std::string_view childKey, unsigned int n) const { + unsigned int count = 0; + for (const auto& element : this->keyvalues) { + if (sourcepp::string::iequals(element.getKey(), childKey)) { + if (count == n) { + return element; + } + ++count; + } + } + return Element::getInvalid(); +} + +BSPEntityKeyValues::Element& BSPEntityKeyValues::operator()(std::string_view childKey, unsigned int n) { + unsigned int count = 0; + for (auto& element: this->keyvalues) { + if (sourcepp::string::iequals(element.getKey(), childKey)) { + if (count == n) { + return element; + } + ++count; + } + } + return this->addKeyValue(childKey); +} + +std::string BSPEntityKeyValues::bake(bool useEscapes) const { + std::string out = "{\n"; + for (const auto& elem : this->keyvalues) { + out += "\t\""; + out += parser::text::convertSpecialCharsToEscapes(elem.getKey(), useEscapes ? parser::text::DEFAULT_ESCAPE_SEQUENCES : parser::text::NO_ESCAPE_SEQUENCES); + out += "\" \""; + out += parser::text::convertSpecialCharsToEscapes(elem.getValue(), useEscapes ? parser::text::DEFAULT_ESCAPE_SEQUENCES : parser::text::NO_ESCAPE_SEQUENCES); + out += "\"\n"; + } + out += "}"; + return out; +} diff --git a/src/bsppp/PakLump.cpp b/src/bsppp/PakLump.cpp index efbc95df8..8a803bb8d 100644 --- a/src/bsppp/PakLump.cpp +++ b/src/bsppp/PakLump.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include #include @@ -42,7 +42,7 @@ std::unique_ptr PakLump::open(const std::string& path, const EntryCall bsp->version = reader.getVersion(); bsp->mapRevision = reader.getMapRevision(); - if (auto pakFileLump = reader.readLump(BSPLump::PAKFILE)) { + if (auto pakFileLump = reader.getLumpData(BSPLump::PAKFILE)) { // Extract the paklump to a temp file FileStream writer{bsp->tempPakLumpPath, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT}; writer.write(*pakFileLump); @@ -111,7 +111,8 @@ bool PakLump::bake(const std::string& outputDir_, BakeOptions options, const Ent if (!writer) { return false; } - writer.writeLump(BSPLump::PAKFILE, fs::readFileBuffer(this->tempZIPPath), false); + writer.setLump(BSPLump::PAKFILE, 0, fs::readFileBuffer(this->tempZIPPath)); + writer.bake(); } // Rename and reopen the ZIP diff --git a/src/bsppp/_bsppp.cmake b/src/bsppp/_bsppp.cmake index f3fc447a0..dea095906 100644 --- a/src/bsppp/_bsppp.cmake +++ b/src/bsppp/_bsppp.cmake @@ -1,9 +1,15 @@ add_pretty_parser(bsppp - DEPS MINIZIP::minizip sourcepp_parser sourcepp::vpkpp + DEPS liblzma MINIZIP::minizip sourcepp_parser + DEPS_PUBLIC sourcepp::vpkpp PRECOMPILED_HEADERS + "${CMAKE_CURRENT_SOURCE_DIR}/include/bsppp/BSP.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/bsppp/bsppp.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/bsppp/EntityLump.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/bsppp/LumpData.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/bsppp/PakLump.h" SOURCES - "${CMAKE_CURRENT_LIST_DIR}/bsppp.cpp" + "${CMAKE_CURRENT_LIST_DIR}/BSP.cpp" + "${CMAKE_CURRENT_LIST_DIR}/EntityLump.cpp" "${CMAKE_CURRENT_LIST_DIR}/PakLump.cpp") + +target_include_directories(bsppp PRIVATE "${xz_SOURCE_DIR}/src/liblzma/api") diff --git a/src/bsppp/bsppp.cpp b/src/bsppp/bsppp.cpp deleted file mode 100644 index df0566558..000000000 --- a/src/bsppp/bsppp.cpp +++ /dev/null @@ -1,359 +0,0 @@ -#include - -#include -#include -#include -#include - -#include - -using namespace bsppp; -using namespace sourcepp; - -namespace { - -template -[[nodiscard]] std::vector readLumpContents(const BSP& bsp, BSPLump lump, void(*callback)(const BSP&, BufferStreamReadOnly&, std::vector&) = [](const BSP&, BufferStreamReadOnly& stream, std::vector& out) { - stream.read(out, stream.size() / sizeof(T)); -}) { - auto data = bsp.readLump(lump); - if (!data) { - return {}; - } - - BufferStreamReadOnly stream{*data}; - std::vector out; - callback(bsp, stream, out); - return out; -} - -template -requires requires(New) { {New::upgrade(Old{})} -> std::same_as; } -void readAndUpgrade(BufferStreamReadOnly& stream, std::vector& out) { - std::vector old; - stream.read(old, stream.size() / sizeof(Old)); - for (const auto& elem : old) { - out.push_back(New::upgrade(elem)); - } -} - -} // namespace - -BSP::BSP(std::string path_) - : path(std::move(path_)) { - FileStream reader{this->path}; - if (!reader) { - this->path.clear(); - return; - } - - if (auto signature = reader.seek_in(0).read(); signature != BSP_SIGNATURE) { - // File is not a BSP - this->path.clear(); - return; - } - this->header.version = reader.read(); - - // Contagion funny - if (this->header.version == 27) { - reader.skip_in(); - } - - reader >> this->header.lumps; - - // If no offsets are larger than 1024 (less than the size of the BSP header, but greater than any lump version), - // it's probably a L4D2 BSP and those are lump versions! - if (this->header.version == 21) { - int i; - for (i = 0; i < BSP_LUMP_COUNT; i++) { - if (this->header.lumps[i].offset > 1024) { - break; - } - } - this->isL4D2 = i == BSP_LUMP_COUNT; - if (this->isL4D2) { - // Swap fields around - for (i = 0; i < BSP_LUMP_COUNT; i++) { - std::swap(this->header.lumps[i].offset, this->header.lumps[i].version); - std::swap(this->header.lumps[i].offset, this->header.lumps[i].length); - } - } - } - - reader >> this->header.mapRevision; -} - -BSP::operator bool() const { - return !this->path.empty(); -} - -BSP BSP::create(std::string path, int32_t version, int32_t mapRevision) { - { - FileStream writer{path, FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT}; - writer << BSP_SIGNATURE << BSP::Header{ - .version = version, - .lumps = {}, - .mapRevision = mapRevision, - }; - } - return BSP{std::move(path)}; -} - -int32_t BSP::getVersion() const { - return this->header.version; -} - -void BSP::setVersion(int32_t version) { - this->header.version = version; - this->writeHeader(); -} - -int32_t BSP::getMapRevision() const { - return this->header.mapRevision; -} - -void BSP::setMapRevision(int32_t mapRevision) { - this->header.mapRevision = mapRevision; - this->writeHeader(); -} - -bool BSP::hasLump(BSPLump lumpIndex) const { - if (this->path.empty()) { - return false; - } - const auto lump = static_cast>(lumpIndex); - return this->header.lumps[lump].length != 0 && this->header.lumps[lump].offset != 0; -} - -int32_t BSP::getLumpVersion(BSPLump lumpIndex) const { - if (this->path.empty()) { - return 0; - } - return this->header.lumps[static_cast>(lumpIndex)].version; -} - -void BSP::setLumpVersion(BSPLump lumpIndex, int32_t version) { - this->header.lumps[static_cast>(lumpIndex)].version = version; - this->writeHeader(); -} - -std::optional> BSP::readLump(BSPLump lumpIndex) const { - if (this->path.empty() || !this->hasLump(lumpIndex)) { - return std::nullopt; - } - FileStream reader{this->path}; - return reader - .seek_in(this->header.lumps[static_cast>(lumpIndex)].offset) - .read_bytes(this->header.lumps[static_cast>(lumpIndex)].length); -} - -void BSP::writeLump(BSPLump lumpIndex, std::span data, bool condenseFile) { - if (this->path.empty() || lumpIndex == BSPLump::UNKNOWN) { - return; - } - - auto lumpToMove = static_cast>(lumpIndex); - - if (!this->hasLump(lumpIndex) || !condenseFile) { - // Put the lump at the end of the file - int32_t lastLumpOffset = 0, lastLumpLength = 0; - for (const auto& [lumpOffset, lumpLength, lumpVersion, lumpFourCC] : this->header.lumps) { - if (lastLumpOffset < this->header.lumps[lumpToMove].offset) { - lastLumpOffset = lumpOffset; - lastLumpLength = lumpLength; - } - } - if (lastLumpOffset == 0) { - // Whole file is full of empty lumps - this->header.lumps[lumpToMove].offset = sizeof(Header); - } else { - this->header.lumps[lumpToMove].offset = lastLumpOffset + lastLumpLength; - } - this->header.lumps[lumpToMove].length = static_cast(data.size()); - } else { - // Sort lumps by file position - std::array lumpIDs{}; - for (int i = 0; i < lumpIDs.size(); i++) { - lumpIDs[i] = i; - } - std::sort(lumpIDs.begin(), lumpIDs.end(), [this](int lhs, int rhs) { - return this->header.lumps[lhs].offset < this->header.lumps[rhs].offset; - }); - - // Condense the lumps in the order they appear in the file, and move the new lump to the end - FileStream bsp{this->path, FileStream::OPT_READ | FileStream::OPT_WRITE}; - int32_t currentOffset = 0; - for (int i = 0; i < lumpIDs.size(); i++) { - auto lumpID = lumpIDs[i]; - - if (lumpID == lumpToMove) { - continue; - } - if (!currentOffset) { - currentOffset = this->header.lumps[lumpID].offset + this->header.lumps[lumpID].length; - continue; - } - - auto lumpsData = bsp.seek_in(this->header.lumps[lumpID].offset).read_bytes(this->header.lumps[lumpID].length); - bsp.seek_out(currentOffset).write(lumpsData); - - this->header.lumps[lumpID].offset = currentOffset; - currentOffset += this->header.lumps[lumpID].length; - - // If we have the space to add padding (we should), then do so - // This should never fail for well-constructed BSP files - auto padding = math::paddingForAlignment(4, currentOffset); - if (padding && i < lumpIDs.size() - 1 && currentOffset + padding <= this->header.lumps[lumpIDs[i + 1]].offset) { - currentOffset += padding; - } - } - - this->header.lumps[lumpToMove].offset = currentOffset; - this->header.lumps[lumpToMove].length = static_cast(data.size()); - } - - // Write modified header and lump - this->writeHeader(); - { - FileStream writer{this->path, FileStream::OPT_READ | FileStream::OPT_WRITE}; - writer.seek_out(this->header.lumps[lumpToMove].offset).write(data); - } - - // Resize file if it shrank - int32_t lastLumpOffset = 0, lastLumpLength = 0; - for (const auto& [lumpOffset, lumpLength, lumpVersion, lumpFourCC] : this->header.lumps) { - if (lastLumpOffset < this->header.lumps[lumpToMove].offset) { - lastLumpOffset = lumpOffset; - lastLumpLength = lumpLength; - } - } - if (std::filesystem::file_size(this->path) > lastLumpOffset + lastLumpLength) { - std::filesystem::resize_file(this->path, lastLumpOffset + lastLumpLength); - } -} - -bool BSP::applyLumpPatchFile(const std::string& lumpFilePath) { - if (this->path.empty()) { - return false; - } - - FileStream reader{lumpFilePath}; - if (!reader) { - return false; - } - - const auto offset = reader.read(); - const auto index = reader.read(); - const auto version = reader.read(); - const auto length = reader.read(); - if (index < 0 || index > BSP_LUMP_COUNT || offset <= 0 || length <= 0) { - return false; - } - - this->header.lumps[index].version = version; - this->writeLump(static_cast(index), reader.seek_in(offset).read_bytes(length)); - return true; -} - -void BSP::createLumpPatchFile(BSPLump lumpIndex) const { - auto lumpData = this->readLump(lumpIndex); - if (!lumpData) { - return; - } - - const auto& [lumpOffset, lumpLength, lumpVersion, lumpFourCC] = this->header.lumps.at(static_cast>(lumpIndex)); - - const auto fsPath = std::filesystem::path{this->path}; - const auto fsStem = (fsPath.parent_path() / fsPath.stem()).string() + "_l_"; - int nonexistentNumber; - for (nonexistentNumber = 0; true; nonexistentNumber++) { - if (!std::filesystem::exists(fsStem + std::to_string(nonexistentNumber) + ".lmp")) { - break; - } - } - - FileStream writer{fsStem + std::to_string(nonexistentNumber) + ".lmp", FileStream::OPT_TRUNCATE | FileStream::OPT_CREATE_IF_NONEXISTENT}; - writer - .seek_out(0) - .write(sizeof(int32_t) * 5) - .write(lumpIndex) - .write(lumpVersion) - .write(lumpLength) - .write(this->header.mapRevision) - .write(*lumpData); -} - -void BSP::writeHeader() const { - FileStream writer{this->path, FileStream::OPT_READ | FileStream::OPT_WRITE}; - writer.seek_out(0) << BSP_SIGNATURE << this->header.version; - - // Contagion funny - if (this->header.version == 27) { - writer.write(0); - } - - if (!this->isL4D2) { - writer << this->header.lumps; - } else { - for (int i = 0; i < static_cast>(BSPLump::COUNT); i++) { - writer << this->header.lumps[i].version << this->header.lumps[i].offset << this->header.lumps[i].length << this->header.lumps[i].fourCC; - } - } - - writer << this->header.mapRevision; -} - -std::vector BSP::readPlanes() const { - return ::readLumpContents(*this, BSPLump::PLANES); -} - -std::vector BSP::readTextureData() const { - return ::readLumpContents(*this, BSPLump::TEXDATA); -} - -std::vector BSP::readVertices() const { - return ::readLumpContents(*this, BSPLump::VERTEXES); -} - -std::vector BSP::readTextureInfo() const { - return ::readLumpContents(*this, BSPLump::TEXINFO); -} - -std::vector BSP::readFaces() const { - return ::readLumpContents(*this, BSPLump::FACES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector& out) { - if (bsp.getLumpVersion(BSPLump::FACES) > 1) { - stream.read(out, stream.size() / sizeof(BSPFace_v2)); - } else { - ::readAndUpgrade(stream, out); - } - }); -} - -std::vector BSP::readEdges() const { - return ::readLumpContents(*this, BSPLump::EDGES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector& out) { - if (bsp.getLumpVersion(BSPLump::EDGES) > 0) { - stream.read(out, stream.size() / sizeof(BSPEdge_v1)); - } else { - ::readAndUpgrade(stream, out); - } - }); -} - -std::vector BSP::readSurfEdges() const { - return ::readLumpContents(*this, BSPLump::SURFEDGES); -} - -std::vector BSP::readBrushModels() const { - return ::readLumpContents(*this, BSPLump::MODELS); -} - -std::vector BSP::readOriginalFaces() const { - return ::readLumpContents(*this, BSPLump::ORIGINALFACES, [](const BSP& bsp, BufferStreamReadOnly& stream, std::vector& out) { - // ORIGINALFACES lump version is always 0? - if (bsp.getLumpVersion(BSPLump::FACES) > 1) { - stream.read(out, stream.size() / sizeof(BSPFace_v2)); - } else { - ::readAndUpgrade(stream, out); - } - }); -}