--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] -- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/ -- All Rights Reserved. Not allowed to be reuploaded. -- License: https://github.com/Nak2/NikNaks/blob/main/LICENSE local obj_tostring = "BSP %s [ %s ]" local format, clamp, min, max = string.format, math.Clamp, math.min, math.max --- @class BSPObject local meta = NikNaks.__metatables["BSP"] --- @class LeafObject local meta_leaf = {} meta_leaf.__index = meta_leaf meta_leaf.__tostring = function( self ) return format( obj_tostring, "Leaf", self.__id ) end meta_leaf.MetaName = "BSP Leaf" NikNaks.__metatables["BSP Leaf"] = meta_leaf local MAX_MAP_NODES = 65536 local TEST_EPSILON = 0.01 local canGenerateParents = false --- Generates parentNodes for nodes and leafs. --- @param self BSPObject --- @param nodeNum integer --- @param parent integer --- @param nodes table --- @param leafs table local function makeParents(self, nodeNum, parent, nodes, leafs, firstRun) if firstRun and not canGenerateParents then return end canGenerateParents = false nodes[nodeNum].parentNode = parent for i = 1, 2 do local j = nodes[nodeNum].children[i] if j < 0 then leafs[-j - 1].parentNode = nodeNum; else makeParents(self, j, nodeNum, nodes, leafs) end end end --- Returns a table of map nodes --- @return MapNode[] function meta:GetNodes() if self._node then return self._node end self._node = {} local data = self:GetLump( 5 ) for i = 0, math.min( data:Size() / 256, MAX_MAP_NODES ) - 1 do --- @class MapNode --- @field children number[] local t = {} t.planenum = data:ReadLong() t.plane = self:GetPlanes()[ t.planenum ] t.children = { data:ReadLong(), data:ReadLong() } t.mins = Vector( data:ReadShort(), data:ReadShort(), data:ReadShort() ) t.maxs = Vector( data:ReadShort(), data:ReadShort(), data:ReadShort() ) t.firstFace = data:ReadUShort() t.numFaces = data:ReadUShort() t.area = data:ReadShort() t.padding = data:ReadShort() self._node[i] = t end self:ClearLump( 5 ) canGenerateParents = true makeParents(self, 0, -1, self._node, self:GetLeafs(), true) canGenerateParents = false return self._node end --- Returns a table of map leafs. --- @return LeafObject[], number num_clusters function meta:GetLeafs() if self._leafs then return self._leafs, self._leafs_num_clusters end --- @type LeafObject[] self._leafs = {} local lumpversion = self:GetLumpVersion( 10 ) local data = self:GetLump( 10 ) local size = 240 -- version if lumpversion == 0 then size = size + 192 -- byte r, byte g, byte b + char expo end if self._version <= 19 or true then size = size + 16 end local n = 0 for i = 0, data:Size() / size - 1 do data:Seek( i * size ) --- @class LeafObject --- @field mins Vector --- @field maxs Vector local t = {} t.contents = data:ReadLong() -- 32 32 4 t.cluster = data:ReadShort() -- 16 48 6 n = math.max( t.cluster + 1, n ) local d = data:ReadUShort() t.area = bit.band( d, 0x1FF ) -- 16 64 8 t.flags = bit.rshift( d, 9 ) -- 16 80 10 t.mins = Vector( data:ReadShort(), data:ReadShort(), data:ReadShort() ) -- 16 x 3 ( 48 ) 128 16 t.maxs = Vector( data:ReadShort(), data:ReadShort(), data:ReadShort() ) -- 16 x 3 ( 48 ) 176 22 t.firstleafface = data:ReadUShort() -- 16 192 24 t.numleaffaces = data:ReadUShort() -- 16 208 26 t.firstleafbrush = data:ReadUShort() -- 16 224 28 t.numleafbrushes = data:ReadUShort() -- 16 240 30 t.leafWaterDataID = data:ReadShort() -- 16 256 32 t.__id = i t.__map = self t.parentNode = -1 if t.leafWaterDataID > -1 then t.leafWaterData = self:GetLeafWaterData()[t.leafWaterDataID] end setmetatable( t, meta_leaf ) self._leafs[i] = t end self._leafs_num_clusters = n self:ClearLump( 10 ) canGenerateParents = true makeParents(self, 0, -1, self:GetNodes(), self._leafs, true) canGenerateParents = false return self._leafs, n end --- Returns a list of LeafWaterData. Holds the data of leaf nodes that are inside water. --- @return LeafWaterData[] function meta:GetLeafWaterData() if self._pLeafWaterData then return self._pLeafWaterData end local data = self:GetLump( 36 ) self._pLeafWaterData = {} for i = 0, data:Size() / 80 - 1 do --- @class LeafWaterData local t = {} t.surfaceZ = data:ReadFloat() t.minZ = data:ReadFloat() t.surfaceTexInfoID = data:ReadShort() data:Skip( 2 ) -- A short that is always 0x00 self._pLeafWaterData[i] = t end self:ClearLump( 36 ) return self._pLeafWaterData end --- Returns the number of leaf-clusters --- @return number function meta:GetLeafsNumClusters() local _, num_clusters = self:GetLeafs() return num_clusters end local mat = Material( "vgui/menu_mode_border" ) local defaultColor = Color( 255, 0, 0, 255 ) --- A simple debug-render function that renders the leaf --- @CLIENT --- @param col Color function meta_leaf:DebugRender( col ) render.SetMaterial( mat ) render.SetBlend( 0.8 ) render.DrawBox( Vector( 0, 0, 0 ), Angle( 0, 0, 0 ), self.maxs, self.mins, col or defaultColor ) render.SetBlend( 1 ) end --- Returns the leaf index. --- @return number function meta_leaf:GetIndex() return self.__id or -1 end --- Returns the leaf area. --- @return number function meta_leaf:GetArea() return self.area or -1 end ---In most cases, leafs within the skybox share the same value and are have the cluster id of 0. -- However older Source versions doesn't have 3D skyboxes and untested on maps without 3D skybox. --function meta_leaf:In3DSkyBox() -- return self.cluster == 0 --end --- Returns true if the leaf has the 3D sky within its PVS. --- Note: Seems to be broken from EP2 and up. --- @return boolean function meta_leaf:HasSkyboxInPVS() return bit.band( self.flags, 0x1 ) ~= 0 end --- Returns true if the leaf has the 3D sky within its PVS. --- Note: Seems to be deprecated. Use Leaf:HasSkyboxInPVS() and BSP:HasSkyBox() instead. --- @return boolean function meta_leaf:Has2DSkyboxInPVS() return bit.band( self.flags, 0x4 ) ~= 0 end --- Returns true if the leaf has said content --- @return boolean function meta_leaf:HasContents( CONTENTS ) if CONTENTS == 0 then return self.contents == CONTENTS end return bit.band( self.contents, CONTENTS ) ~= 0 end --- Returns the content flag the leaf has. --- @return number function meta_leaf:GetContents() return self.contents end --- Returns a list of faces within this leaf. Starting at 1. --- Note: A face can be in multiple leafs. --- @return FaceObject[] function meta_leaf:GetFaces() if self._faces then return self._faces end --- @type FaceObject[] self._faces = {} local faces = self.__map:GetFaces() local leafFace = self.__map:GetLeafFaces() local c = self.firstleafface for i = 0, self.numleaffaces do local f_id = leafFace[ i + c ] self._faces[i + 1] = faces[f_id] end return self._faces end --- Returns true if the leaf has water within. --- @return boolean function meta_leaf:HasWater() return self.leafWaterDataID > 0 end --- Returns the water data, if any. --- @return table|nil function meta_leaf:GetWaterData() return self.leafWaterData end --- Returns the water MaxZ within the leaf. --- @return number|nil function meta_leaf:GetWaterMaxZ() return self.leafWaterData and self.leafWaterData.surfaceZ end --- Returns the water MinZ within the leaf. --- @return number|nil function meta_leaf:GetWaterMinZ() return self.leafWaterData and self.leafWaterData.minZ end --- Returns true if the leaf is outside the map. --- @return boolean function meta_leaf:IsOutsideMap() -- Locations outside the map are always cluster -1. However we check to see if the contnets is solid to be sure. return self.cluster == -1 and self.contents == 1 end --- Returns the cluster-number for the leaf. Cluster numbers can be shared between multiple leafs. --- @return number function meta_leaf:GetCluster() return self.cluster end --- Returns true if the position is within the given leaf. --- @param position Vector --- @return boolean function meta_leaf:IsPositionWithin( position ) local l = self.__map:PointInLeaf(0, position) if not l then return false end return l:GetIndex() == self:GetIndex() end --- Returns a list of all leafs around the given leaf. --- @param range? Number --- @return table function meta_leaf:GetAdjacentLeafs() local t, i, s = {}, 1, 2 for _, leaf in ipairs( self.__map:AABBInLeafs(0, self.mins, self.maxs, s) ) do if leaf == self then continue end t[i] = leaf i = i + 1 end return t end --- Returns true if the leafs are adjacent to each other. --- @return bool function meta_leaf:IsLeafAdjacent( leaf ) for _, c_leaf in ipairs( self:GetAdjacentLeafs() ) do if c_leaf == leaf then return true end end return false end --- Roughly returns the distance from leaf to the given position. --- @param position Vector --- @return number function meta_leaf:Distance( position ) local cPos = Vector(clamp(position.x, self.mins.x, self.maxs.x), clamp(position.y, self.mins.y, self.maxs.y), clamp(position.z, self.mins.z, self.maxs.z)) return cPos:Distance( position ) end --- Roughly returns the distance from leaf to the given position. --- @param position Vector --- @return number function meta_leaf:DistToSqr( position ) local cPos = Vector(clamp(position.x, self.mins.x, self.maxs.x), clamp(position.y, self.mins.y, self.maxs.y), clamp(position.z, self.mins.z, self.maxs.z)) return cPos:DistToSqr( position ) end --- Returns a list of planes, pointing into the leaf. --- @return Plane[] function meta_leaf:GetBoundaryPlanes() local nodeIndex = self.parentNode local list = {} if not nodeIndex then return t end local child = -( self:GetIndex() + 1 ) local nodes = self.__map:GetNodes() while ( nodeIndex >= 0 ) do local node = nodes[nodeIndex] local plane = node.plane if( node.children[1] == child ) then table.insert(list, plane) else table.insert(list, { dist = -plane.dist, normal = -plane.normal, type = plane.type }) end child = nodeIndex nodeIndex = nodes[child].parentNode end return list end local function locateBoxLeaf( iNode, tab, mins, maxs, nodes, planes, leafs ) local cornerMin, cornerMax = Vector(0,0,0), Vector(0,0,0) while iNode >= 0 do local node = nodes[ iNode ] local plane = planes[ node.planenum ] for i = 1, 3 do if( plane.normal[i] >= 0) then cornerMin[i] = mins[i] cornerMax[i] = maxs[i] else cornerMin[i] = maxs[i] cornerMax[i] = mins[i] end end if plane.normal:Dot(cornerMax) - plane.dist <= -TEST_EPSILON then iNode = node.children[2] elseif plane.normal:Dot(cornerMin) - plane.dist >= TEST_EPSILON then iNode = node.children[1] else if not locateBoxLeaf(node.children[1], tab, mins, maxs, nodes, planes, leafs) then return false end return locateBoxLeaf(node.children[2], tab, mins, maxs, nodes, planes, leafs) end end tab[#tab + 1] = leafs[ -1 -iNode ] return true end --- Returns a list of leafs within the given two positions. --- @param iNode? number --- @param point Vector --- @param point2 Vector --- @param add? number --- @return table function meta:AABBInLeafs( iNode, point, point2, add ) add = add or 0 local mins = Vector(min(point.x, point2.x) - add, min(point.y, point2.y) - add, min(point.z, point2.z) - add) local maxs = Vector(max(point.x, point2.x) + add, max(point.y, point2.y) + add, max(point.z, point2.z) + add) local tab = {} locateBoxLeaf(iNode or 0, tab, mins, maxs, self:GetNodes(), self:GetPlanes(), self:GetLeafs()) return tab end ---Returns true if the AABB is outside the map --- @param position Vector --- @param position2 Vector --- @return boolean function meta:IsAABBOutsideMap( position, position2 ) for _, leaf in pairs( self:AABBInLeafs( 0, position, position2 ) ) do if leaf:IsOutsideMap() then return true end end return false end local function locateSphereLeaf( iNode, tab, origin, radius, nodes, planes, leafs) while iNode >= 0 do local node = nodes[ iNode ] local plane = planes[ node.planenum ] if plane.normal:Dot(origin) + radius - plane.dist <= -TEST_EPSILON then iNode = node.children[2] elseif plane.normal:Dot(origin) - radius - plane.dist >= TEST_EPSILON then iNode = node.children[1] else if not locateSphereLeaf( node.children[1], tab, origin, radius, nodes, planes, leafs ) then return false end return locateSphereLeaf( node.children[2], tab, origin, radius, nodes, planes, leafs ) end end tab[#tab + 1] = leafs[ -1 -iNode ] return true end --- Returns a list of leafs within the given sphere. --- @param iNode number --- @param origin Vector --- @param radius number --- @return table function meta:SphereInLeafs(iNode, origin, radius) local tab = {} locateSphereLeaf(iNode, tab, origin, radius, self:GetNodes(), self:GetPlanes(), self:GetLeafs()) return tab end --- Returns true if the sphere is outside the map --- @param position Vector --- @param range number --- @return boolean function meta:IsSphereOutsideMap( position, range ) for _, leaf in pairs( self:SphereInLeafs( 0, position, range ) ) do if leaf:IsOutsideMap() then return true end end return false end --- Returns roughtly the leafs maximum boundary --- @return Vector function meta_leaf:OBBMaxs() return self.maxs end --- Returns roughtly the leafs maximum boundary --- @return Vector function meta_leaf:OBBMins() return self.mins end --- Returns roughtly the leafs center. --- @return Vector function meta_leaf:GetPos() return (self.mins + self.maxs) / 2 end