Files
wnsrc/lua/niknaks/modules/sh_bsp_module.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

1650 lines
43 KiB
Lua

--[[
| 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 NikNaks = NikNaks
NikNaks.Map = {}
local obj_tostring = "BSP %s [ %s ]"
local format = string.format
--- @class BSPObject
local meta = NikNaks.__metatables["BSP"]
--- @return File
local function openFile( self )
assert( self._mapfile, "BSP object has nil mapfile path!" )
return file.Open( self._mapfile, "rb", "GAME" )
end
--- Reads the lump header.
--- @param self BSPObject
--- @param f BitBuffer
local function read_lump_h( self, f )
-- "How do we stop people loading L4D2 maps in other games?"
-- "I got it, we scrample the header."
--- @class BSPLumpHeader
local t = {}
if self._version ~= 21 or self._isL4D2 == false then
t.fileofs = f:ReadLong()
t.filelen = f:ReadLong()
t.version = f:ReadLong()
t.fourCC = f:ReadLong()
elseif self._isL4D2 == true then
t.version = f:ReadLong()
t.fileofs = f:ReadLong()
t.filelen = f:ReadLong()
t.fourCC = f:ReadLong()
elseif NikNaks._Source:find( "niknak" ) then -- Try and figure it out
local fileofs = f:ReadLong() -- Version
local filelen = f:ReadLong() -- fileofs
local version = f:ReadLong() -- filelen
t.fourCC = f:ReadLong()
if fileofs <= 8 then
self._isL4D2 = true
t.version = fileofs
t.fileofs = filelen
t.filelen = version
else
self._isL4D2 = false
t.fileofs = fileofs
t.filelen = filelen
t.version = version
end
end
return t
end
--- Parse LZMA. These are for gamelumps, entities, PAK files and staticprops .. ect from TF2
--- @param str string
--- @return string
local function LZMADecompress( str )
if str:sub( 0, 4 ) ~= "LZMA" then return str end
local actualSize = str:sub( 5, 8 )
local lzmaSize = NikNaks.BitBuffer.StringToInt( str:sub( 9, 12 ) )
if lzmaSize <= 0 then return "" end -- Invalid length
local t = str:sub( 13, 17 )
local data = str:sub( 18, 18 + lzmaSize ) -- Why not just read all of it? What data is after this? Tell me your secrets Valve.
return util.Decompress( t .. actualSize .. "\0\0\0\0" .. data ) or str
end
local thisMap = "maps/" .. game.GetMap() .. ".bsp"
local thisMapObject
--- Reads the BSP file and returns it as an object.
--- @param fileName string
--- @return BSPObject
--- @return BSP_ERROR_CODE?
function NikNaks.Map( fileName )
-- Handle filename
if not fileName then
if thisMapObject then return thisMapObject end -- Return this map
fileName = thisMap
else
if not string.match( fileName, ".bsp$" ) then fileName = fileName .. ".bsp" end -- Add file header
if not string.match( fileName, "^maps/" ) and not file.Exists( fileName, "GAME" ) then -- Map doesn't exists and no folder indecated.
fileName = "maps/" .. fileName -- Add "maps" folder
end
end
if not file.Exists( fileName, "GAME" ) then
-- File not found
return nil, NikNaks.BSP_ERROR_FILENOTFOUND
end
local f = file.Open( fileName, "rb", "GAME" )
if not f then
-- Unable to open file
return nil, NikNaks.BSP_ERROR_FILECANTOPEN
end
-- Read the header
if f:Read( 4 ) ~= "VBSP" then
f:Close()
return nil, NikNaks.BSP_ERROR_NOT_BSP
end
-- Create BSP object
--- @class BSPObject
local BSP = setmetatable( {}, meta )
BSP._mapfile = fileName
BSP._size = f:Size()
BSP._mapname = string.GetFileFromFilename( fileName )
BSP._mapname = string.match( BSP._mapname, "(.+).bsp$" ) or BSP._mapname
BSP._version = f:ReadLong()
BSP._fileobj = f
if BSP._version > 21 then
f:Close()
return nil, NikNaks.BSP_ERROR_TOO_NEW
end
-- Read Lump Header
--- @type BSPLumpHeader[]
BSP._lumpheader = {}
for i = 0, 63 do
BSP._lumpheader[i] = read_lump_h( BSP, f )
end
--- @type BitBuffer[]
BSP._lumpstream = {}
BSP._gamelumps = {}
f:Close()
if thisMap == fileName then
thisMapObject = BSP
end
return BSP
end
-- Smaller functions
do
--- Returns the mapname.
--- @return string
function meta:GetMapName()
return self._mapname or "Unknown"
end
--- Returns the mapfile.
--- @return string
function meta:GetMapFile()
return self._mapfile or "No file"
end
--- Returns the map-version.
--- @return number
function meta:GetVersion()
return self._version
end
--- Returns the size of the map in bytes.
--- @return number
function meta:GetSize()
return self._size
end
end
-- Lump functions
do
if SERVER then
---Returns true if the server has a lumpfile for the given lump
---@param lump_id number
---@return bool
---@server
function meta:HasLumpFile( lump_id )
if self._lumpfile and self._lumpfile[lump_id] ~= nil then
return self._lumpfile[lump_id]
end
self._lumpfile = self._lumpfile or {}
self._lumpfile[lump_id] = file.Exists("maps/" .. self._mapname .. "_l_" .. lump_id .. ".lmp", "GAME")
return self._lumpfile[lump_id]
end
end
-- A list of lumps that are known to be LZMA compressed for TF2 / other. In theory we could apply it to everything
-- However there might be some rare cases where the data start with "LZMA", and trigger this.
--- Returns the data lump as a bytebuffer. This will also be cached onto the BSP object.
--- @param lump_id number
--- @return BitBuffer
function meta:GetLump( lump_id )
local lumpStream = self._lumpstream[lump_id]
if lumpStream then
lumpStream:Seek( 0 ) -- Reset the read position
return lumpStream
end
local lump_h = self._lumpheader[lump_id]
assert( lump_h, "Tried to read invalid lumpheader!" )
-- The raw lump data
local data
-- Check for LUMPs
local lumpPath = "maps/" .. self._mapname .. "_l_" .. lump_id .. ".lmp"
if file.Exists( lumpPath, "GAME" ) then -- L4D has _s_ and _h_ files too. Depending on the gamemode.
data = file.Read( lumpPath, "GAME" )
elseif lump_h.filelen > 0 then
local f = openFile( self )
f:Seek( lump_h.fileofs )
data = f:Read( lump_h.filelen )
f:Close()
else
data = ""
end
-- TF2 have some maps that are LZMA compressed.
data = LZMADecompress( data )
-- Create bytebuffer object with the data and return it
--- @type BitBuffer
self._lumpstream[lump_id] = NikNaks.BitBuffer( data or "" )
return self._lumpstream[lump_id]
end
--- Deletes lump_data cached in the BSP module.
--- @param lump_id number
function meta:ClearLump( lump_id )
self._lumpstream[lump_id] = nil
end
--- Returns the lump version.
--- @return number
function meta:GetLumpVersion( lump_id )
return self._lumpheader[ lump_id ].version
end
--- Returns the data lump as a datastring.
--- This won't be cached or saved, but it is faster than to parse the data into a bytebuffer and useful if you need the raw data.
--- @param lump_id number
--- @return string
function meta:GetLumpString( lump_id )
local lump_h = self._lumpheader[lump_id]
assert( lump_h, "Tried to read invalid lumpheader!" )
-- The raw lump data
local data
local lumpPath = "maps/" .. self._mapname .. "_l_" .. lump_id .. ".lmp"
if file.Exists( lumpPath, "GAME" ) then
data = file.Read( lumpPath, "GAME" )
elseif lump_h.filelen > 0 then
local f = openFile( self )
f:Seek( lump_h.fileofs )
data = f:Read( lump_h.filelen )
f:Close()
else
data = ""
end
-- TF2 have some maps that are LZMA compressed.
data = LZMADecompress( data )
return data
end
--- Returns a list of gamelumps.
--- @return BSPLumpHeader[]
function meta:GetGameLumpHeaders()
if self._gamelump then return self._gamelump end
self._gamelump = {}
local lump = self:GetLump( 35 )
for i = 0, math.min( 63, lump:ReadLong() ) do
--- @class BSPLumpHeader
local t = {
id = lump:ReadLong(),
flags = lump:ReadUShort(),
version = lump:ReadUShort(),
fileofs = lump:ReadLong(),
filelen = lump:ReadLong()
}
self._gamelump[i] = t
end
self:ClearLump( 35 )
return self._gamelump
end
--- Returns gamelump number, matching the gLumpID.
--- @param GameLumpID number
--- @return BSPLumpHeader
function meta:FindGameLump( GameLumpID )
for _, v in pairs( self:GetGameLumpHeaders() ) do
if v.id == GameLumpID then
return v
end
end
end
--- @class GameLump
--- @field buffer BitBuffer
--- @field version number
--- @field flags number
--- Returns the game lump as a bytebuffer. This will also be cached on the BSP object.
--- @param gameLumpID any
--- @return GameLump
function meta:GetGameLump( gameLumpID )
local gameLump = self._gamelumps[gameLumpID]
if gameLump then
gameLump.buffer:Seek( 0 )
return gameLump
end
-- Locate the gamelump.
local t = self:FindGameLump( gameLumpID )
-- No data found, or lump has no data.
if not t or t.filelen <= 0 then
-- Create an empty bitbuffer with -1 version and 0 flag
gameLump = {
flags = t and t.flags or 0,
version = t and t.version or -1,
buffer = NikNaks.BitBuffer.Create(),
}
self._gamelumps[gameLumpID] = gameLump
return gameLump
else
local f = openFile( self )
f:Seek( t.fileofs )
gameLump = {
flags = t.flags,
version = t.version,
buffer = NikNaks.BitBuffer.Create( LZMADecompress( f:Read( t.filelen ) ) ),
}
self._gamelumps[gameLumpID] = gameLump
return gameLump
end
end
end
-- Word Data
do
local default = [[detail\detailsprites.vmt]]
--- Returns the detail-metail the map uses.
--- @return string
function meta:GetDetailMaterial()
local wEnt = self:GetEntities()[0]
return wEnt and wEnt.detailmaterial or default
end
--- Returns true if the map is a cold world. ( Flag set in the BSP )
--- @return boolean
function meta:IsColdWorld()
local wEnt = self:GetEntity( 0 )
return wEnt and wEnt.coldworld == 1 or false
end
--- Returns the min-positions where brushes are within the map.
--- @return Vector
function meta:WorldMin()
if self._wmin then return self._wmin end
local wEnt = self:GetEntity( 0 )
if not wEnt then
self._wmin = NikNaks.vector_zero
return self._wmin
end
self._wmin = util.StringToType( wEnt.world_mins or "0 0 0", "Vector" ) or NikNaks.vector_zero
return self._wmin
end
--- Returns the max-position where brushes are within the map.
--- @return Vector
function meta:WorldMax()
if self._wmax then return self._wmax end
local wEnt = self:GetEntity( 0 )
if not wEnt then
self._wmax = NikNaks.vector_zero
return self._wmax
end
self._wmax = util.StringToType( wEnt.world_maxs or "0 0 0", "Vector" ) or NikNaks.vector_zero
return self._wmax
end
--- Returns the map-bounds. These are not the size of the map, but the bounds where brushes are within.
--- @return Vector
--- @return Vector
function meta:GetBrushBounds()
return self:WorldMin(), self:WorldMax()
end
--- Returns the skybox position. Returns [0,0,0] if none are found.
--- @return Vector
function meta:GetSkyBoxPos()
if self._skyCamPos then return self._skyCamPos end
local t = self:FindByClass( "sky_camera" )
if #t < 1 then
self._skyCamPos = NikNaks.vector_zero
else
self._skyCamPos = t[1].origin
end
return self._skyCamPos
end
--- Returns the skybox scale. Returns 1 if none are found.
--- @return number
function meta:GetSkyBoxScale()
if self._skyCamScale then return self._skyCamScale end
local t = self:FindByClass( "sky_camera" )
if #t < 1 then
self._skyCamScale = 1
else
self._skyCamScale = t[1].scale
end
return self._skyCamScale
end
--- Returns true if the map has a 3D skybox.
--- @return boolean
function meta:HasSkyBox()
if self._skyCam ~= nil then return self._skyCam end
self._skyCam = #self:FindByClass( "sky_camera" ) > 0
return self._skyCam
end
--- Returns a position in the skybox that matches the one in the world.
--- @param vec Vector
--- @return Vector
function meta:WorldToSkyBox( vec )
return vec / self:GetSkyBoxScale() + self:GetSkyBoxPos()
end
--- Returns a position in the world that matches the one in the skybox.
--- @param vec Vector
--- @return Vector
function meta:SkyBoxToWorld( vec )
return ( vec - self:GetSkyBoxPos() ) * self:GetSkyBoxScale()
end
end
-- Cubemaps
do
--- @class CubeMap
local cubemeta = {}
cubemeta.__index = cubemeta
cubemeta.__tostring = function( self ) return
format( obj_tostring, "Cubemap", "Index: " .. self:GetIndex() )
end
function cubemeta:GetPos() return self.origin end
function cubemeta:GetSize() return self.size end
function cubemeta:GetIndex() return self.id or -1 end
function cubemeta:GetTexture() return self.texture end
--- Returns the CubeMaps in the map.
--- @return CubeMap[]
function meta:GetCubemaps()
if self._cubemaps then return self._cubemaps end
local b = self:GetLump( 42 )
local len = b:Size()
--- @type CubeMap[]
self._cubemaps = {}
for _ = 1, math.min( 1024, len / 128 ) do
--- @class CubeMap
local t = setmetatable( {}, cubemeta )
t.origin = Vector( b:ReadLong(), b:ReadLong(), b:ReadLong() )
t.size = b:ReadLong()
t.texture = ""
local texturePath = "maps/" .. self:GetMapName() .. "/c" .. t.origin.x .. "_" .. t.origin.y .. "_" .. t.origin.z
t.texturePath = texturePath
if self:GetVersion() > 19 then
t.texturePath = texturePath .. ".hdr"
end
if t.size == 0 then
t.size = 32
end
t.id = table.insert( self._cubemaps, t ) - 1
end
self:ClearLump( 42 )
return self._cubemaps
end
--- Returns the nearest cubemap to a position.
--- @param pos Vector
--- @return CubeMap
function meta:FindNearestCubemap( pos )
local lr, lc
for _, v in ipairs( self:GetCubemaps() ) do
local cd = v:GetPos():DistToSqr( pos )
if not lc then
lc = v
lr = cd
elseif lr > cd then
lc = v
lr = cd
end
end
return lc
end
end
-- Textures and materials
do
-- local max_data = 256000
--- @return number[]
function meta:GetTexdataStringTable()
if self._texstab then return self._texstab end
self._texstab = {}
local data = self:GetLump( 44 )
for i = 0, data:Size() / 32 - 1 do
self._texstab[i] = data:ReadLong()
end
self:ClearLump( 44 )
return self._texstab
end
--- @return string[]
function meta:GetTexdataStringData()
if self._texstr then return self._texstr end
--- @type string[]
self._texstr = {}
--- @type string[]
self._texstr.id = {}
local data = self:GetLump( 43 )
for i = 0, data:Size() / 8 - 1 do
local _id = data:Tell() / 8
local str = data:ReadStringNull()
self._texstr.id[_id] = str
if #str == 0 then break end
self._texstr[i] = string.lower( str )
end
self:ClearLump( 43 )
return self._texstr
end
--- @return string[]
function meta:GetTextures()
local c = {}
local q = self:GetTexdataStringData()
for i = 1, #q do
c[i] = q[i]
end
return c
end
local function getTexByID( self, id )
id = self:GetTexdataStringTable()[id]
return self:GetTexdataStringData().id[id]
end
--- Returns a list of material-data used by the map.
--- @return TextureData[]
function meta:GetTexData()
if self._tdata then return self._tdata end
--- @type TextureData[]
self._tdata = {}
-- Load TexdataStringTable
self:GetTextures()
local b = self:GetLump( 2 )
local count = b:Size() / 256 + 1
for i = 0, count - 1 do
--- @class TextureData
local t = {}
t.reflectivity = b:ReadVector()
local n = b:ReadLong()
t.nameStringTableID = getTexByID( self, n ) or tostring( n )
t.width = b:ReadLong()
t.height = b:ReadLong()
t.view_width = b:ReadLong()
t.view_height = b:ReadLong()
self._tdata[i] = t
end
self:ClearLump( 2 )
return self._tdata
end
--- Returns a list of material-data used by the map.
--- @return TextureInfo[]
function meta:GetTexInfo()
if self._tinfo then return self._tinfo end
self._tinfo = {}
local data = self:GetLump( 6 )
for i = 0, data:Size() / 576 - 1 do
--- @class TextureInfo
--- @field textureVects table<number, number[]>
--- @field lightmapVecs table<number, number[]>
local t = {}
t.textureVects = {}
t.textureVects[0] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
t.textureVects[1] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
t.lightmapVecs = {}
t.lightmapVecs[0] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
t.lightmapVecs[1] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
t.flags = data:ReadLong()
t.texdata = data:ReadLong()
self._tinfo[i] = t
end
self:ClearLump( 6 )
return self._tinfo
end
end
-- Planes, Vertex and Edges
do
--- Returns a list of all planes
--- @return Plane[]
function meta:GetPlanes()
if self._plane then return self._plane end
--- @type Plane[]
self._plane = {}
local data = self:GetLump( 1 )
for i = 0, data:Size() / 160 - 1 do
--- @class Plane
local t = {}
t.normal = data:ReadVector() -- Normal vector
t.dist = data:ReadFloat() -- distance form origin
t.type = data:ReadLong() -- plane axis indentifier
self._plane[i] = t
end
self:ClearLump( 1 )
return self._plane
end
local MAX_MAP_VERTEXS = 65536
--- Returns an array of coordinates of all the vertices (corners) of brushes in the map geometry.
--- @return Vector[]
function meta:GetVertex()
if self._vertex then return self._vertex end
--- @type Vector[]
self._vertex = {}
local data = self:GetLump( 3 )
for i = 0, math.min( data:Size() / 96, MAX_MAP_VERTEXS ) - 1 do
self._vertex[i] = data:ReadVector()
end
self:ClearLump( 3 )
return self._vertex
end
local MAX_MAP_EDGES = 256000
--- Returns all edges. An edge is two points forming a line.
--- Note: First edge seems to be [0 0 0] - [0 0 0]
--- @return table<number, Vector[]>
function meta:GetEdges()
if self._edge then return self._edge end
-- @type table<number, Vector[]>
self._edge = {}
local data = self:GetLump( 12 )
local v = self:GetVertex()
for i = 0, math.min( data:Size() / 32, MAX_MAP_EDGES ) -1 do
self._edge[i] = { v[data:ReadUShort()], v[data:ReadUShort()] }
end
self:ClearLump( 12 )
return self._edge
end
local MAX_MAP_SURFEDGES = 512000
---Returns all surfedges. A surfedge is an index to edges with a direction. If positive First -> Second, if negative Second -> First.
---@return number[]
function meta:GetSurfEdges()
if self._surfedge then return self._surfedge end
--- @type number[]
self._surfedge = {}
local data = self:GetLump( 13 )
for i = 0, math.min( data:Size() / 32, MAX_MAP_SURFEDGES ) -1 do
self._surfedge[i] = data:ReadLong()
end
self:ClearLump( 13 )
return self._surfedge
end
local abs = math.abs
--- Returns the two edge-positions using surf index
--- @param num number
--- @return Vector, Vector
function meta:GetSurfEdgesIndex( num )
local surf = self:GetSurfEdges()[num]
local edge = self:GetEdges()[abs( surf )]
if surf >= 0 then
return edge[1], edge[2]
else
return edge[2], edge[1]
end
end
end
-- Visibility, leafbrush and leaf functions
do
--- Returns the visibility data.
--- @return VisibilityInfo
function meta:GetVisibility()
if self._vis then return self._vis end
local data = self:GetLump( 4 )
local num_clusters = data:ReadLong()
-- Check to see if the num_clusters match
if num_clusters ~= self:GetLeafsNumClusters() then
error( "Invalid NumClusters!" )
end
--- @class VisibilityInfo
--- @field VisData VisbilityData[]
local t = { VisData = {} }
local visData = t.VisData
for i = 0, num_clusters - 1 do
--- @class VisbilityData
local v = {}
v.PVS = data:ReadULong()
v.PAS = data:ReadULong()
visData[i] = v
end
data:Seek( 0 )
local bytebuff = {}
for i = 0, bit.rshift( data:Size() - data:Tell(), 3 ) - 1 do
bytebuff[i] = data:ReadByte()
end
t._bytebuff = bytebuff
t.num_clusters = num_clusters
self._vis = t
self:ClearLump( 4 )
return t
end
local TEST_EPSILON = 0.1
--- Returns the leaf the point is within. Use 0 If unsure about iNode.
--- @param iNode number
--- @param point Vector
--- @return LeafObject
function meta:PointInLeaf( iNode, point )
if iNode < 0 then
return self:GetLeafs()[-1 -iNode]
end
local node = self:GetNodes()[iNode]
local plane = self:GetPlanes()[node.planenum]
local dist = point:Dot( plane.normal ) - plane.dist
if dist > TEST_EPSILON then
return self:PointInLeaf( node.children[1], point )
elseif dist < -TEST_EPSILON then
return self:PointInLeaf( node.children[2], point )
else
local pTest = self:PointInLeaf( node.children[1], point )
if pTest.cluster ~= -1 then
return pTest
end
return self:PointInLeaf( node.children[2], point )
end
end
--- Returns the leaf the point is within, but allows caching by feeding the old VisLeaf.
--- Will also return a boolean indicating if the leaf is new.
--- @param iNode number
--- @param point Vector
--- @param lastVis LeafObject?
--- @return LeafObject
--- @return boolean newLeaf
function meta:PointInLeafCache( iNode, point, lastVis )
if not lastVis then return self:PointInLeaf( iNode, point ), true end
if point:WithinAABox( lastVis.mins, lastVis.maxs ) then return lastVis, false end
return self:PointInLeaf( iNode, point ), true
end
--- Returns the vis-cluster from said point.
--- @param position Vector
--- @return number
function meta:ClusterFromPoint( position )
return self:PointInLeaf( 0, position ).cluster or -1
end
--- Computes the leaf-id the detail is within. -1 for none.
--- @param position Vector
--- @return number
function meta:ComputeDetailLeaf( position )
local node = 0
local nodes = self:GetNodes()
local planes = self:GetPlanes()
while node > 0 do
--- @type MapNode
local n = nodes[node]
--- @type Plane
local p = planes[n.planenum]
if position:Dot( p.normal ) < p.dist then
node = n.children[2]
else
node = n.children[1]
end
end
return - node - 1
end
local MAX_MAP_LEAFFACES = 65536
--- Returns the leaf_face array. This is used to return a list of faces from a leaf.
--- FaceID = LeafFace[ Leaf.firstleafface + [0 -> Leaf.numleaffaces - 1] ]
--- @return number[]
function meta:GetLeafFaces()
if self._leaffaces then return self._leaffaces end
--- @type number[]
self._leaffaces = {}
local data = self:GetLump( 16 )
for i = 1, math.min( data:Size() / 16, MAX_MAP_LEAFFACES ) do
self._leaffaces[i] = data:ReadUShort()
end
self:ClearLump( 16 )
return self._leaffaces
end
local MAX_MAP_LEAFBRUSHES = 65536
--- Returns an array of leafbrush-data.
--- @return BrushObject[]
function meta:GetLeafBrushes()
if self._leafbrush then return self._leafbrush end
--- @type BrushObject[]
self._leafbrush = {}
local data = self:GetLump( 17 )
local brushes = self:GetBrushes()
for i = 1, math.min( data:Size() / 16, MAX_MAP_LEAFBRUSHES ) do
self._leafbrush[i] = brushes[data:ReadUShort()]
end
self:ClearLump( 17 )
return self._leafbrush
end
--- Returns map-leafs in a table with cluster-IDs as key. Note: -1 is no cluster ID.
--- @return table<number, LeafObject[]>
function meta:GetLeafClusters()
if self._clusters then return self._clusters end
--- @type table<number, LeafObject[]>
self._clusters = {}
local leafs = self:GetLeafs()
for i = 0, #leafs - 1 do
local leaf = leafs[i]
local cluster = leaf.cluster
if self._clusters[cluster] then
table.insert( self._clusters[cluster], leaf )
else
self._clusters[cluster] = { leaf }
end
end
return self._clusters
end
end
-- Faces and Displacments
do
--- Returns the DispVerts data.
--- @return DispVert[]
function meta:GetDispVerts()
if self._dispVert then return self._dispVert end
--- @type DispVert[]
self._dispVert = {}
local data = self:GetLump( 33 )
for i = 0, data:Size() / 160 - 1 do
--- @class DispVert
local t = {
vec = data:ReadVector(),
dist = data:ReadFloat(),
alpha = data:ReadFloat()
}
self._dispVert[i] = t
end
self:ClearLump( 33 )
return self._dispVert
end
--- Holds flags for the triangle in the displacment mesh.
--- Returns the DispTris data
--- @return number[]
function meta:GetDispTris()
if self._dispTris then return self._dispTris end
--- @type number[]
self._dispTris = {}
local data = self:GetLump( 48 )
for i = 0, data:Size() / 16 - 1 do
self._dispTris[i] = data:ReadUShort()
end
self:ClearLump( 48 )
return self._dispTris
end
local m_AllowedVerts = 10
--- Returns the DispInfo data.
--- @return DispInfo[]
function meta:GetDispInfo()
if self._dispinfo then return self._dispinfo end
--- @type DispInfo[]
self._dispinfo = {}
local data = self:GetLump( 26 )
for i = 0, data:Size() / 1408 - 1 do
--- @class DispInfo
local q = {}
q.startPosition = data:ReadVector()
q.DispVertStart = data:ReadLong()
q.DispTriStart = data:ReadLong()
q.power = data:ReadLong()
q.minTess = data:ReadLong()
q.smoothingAngle = data:ReadFloat()
q.contents = data:ReadLong()
q.MapFace = data:ReadUShort()
q.LightmapAlphaStart = data:ReadLong()
q.LightmapSamplePositionStart = data:ReadLong()
-- 46 bytes used. 130 bytes left regarding corner neighbors .. ect
-- allowedVerts are 40 bytes (10 * 4), therefore neighbors are 90 bytes
data:Skip( 720 ) -- CDispCornerNeighbors + CDispNeighbor
q.allowedVerts = {}
for _ = 0, m_AllowedVerts - 1 do
q.allowedVerts[i] = data:ReadULong()
end
self._dispinfo[i] = q
end
self:ClearLump( 26 )
return self._dispinfo
end
---!! DEBUG FUNCTIONS !!
function meta:GetMaterialMeshs()
if self._materialmesh then return self._materialmesh end
self._materialmesh = {}
-- Build a list of faces-mesh data.
local _meshData = {}
local faces = self:GetFaces()
for i = 1, #faces do
local face = faces[i]
local s = face:GetTexture()
if not _meshData[s] then _meshData[s] = {} end
local _facemesh = face:GenerateVertexTriangleData()
if not _facemesh then continue end
for i = 1, #_facemesh do
table.insert( _meshData[s], _facemesh[i] )
end
end
-- Generate the meshes
for tex, meshData in pairs( _meshData ) do
local _mat = Material( tex )
local _mesh = Mesh( _mat )
table.insert( _MESHBUILD, _mesh )
mesh.Begin( _mesh, MATERIAL_TRIANGLES, #meshData )
for i = 1, #meshData do
local vert = meshData[i]
-- > Mesh
mesh.Normal( vert.normal )
mesh.Position( vert.pos ) -- Set the position
mesh.TexCoord( 0, vert.u, vert.v ) -- Set the texture UV coordinates
mesh.TexCoord( 1, vert.lu, vert.lv ) -- Set the lightmap UV coordinates
mesh.TexCoord( 2, vert.lu, vert.lv ) -- Set the lightmap UV coordinates?
--mesh.TexCoord( 2, self.LightmapTextureSizeInLuxels[1], self.LightmapTextureSizeInLuxels[2] ) -- Set the texture UV coordinates
--mesh.TexCoord( 2, self.LightmapTextureMinsInLuxels[1], self.LightmapTextureMinsInLuxels[2] ) -- Set the texture UV coordinates
mesh.AdvanceVertex()
end
mesh.End()
self._materialmesh[_mat] = _mesh
end
return self._materialmesh
end
-- Do I need thse?
local function BakeTriangles( triangle )
for i = 1, #triangle, 3 do
local A, B, C = triangle[ i ], triangle[ i + 1 ], triangle[ i + 2 ]
local p = A.pos
local edge1 = B.pos - p
local edge2 = C.pos - p
local deltaUV1 = Vector( B.u - A.u, B.v - A.v )
local deltaUV2 = Vector( C.u - A.u, C.v - A.v )
local f = 1 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
local tangent = Vector(0,0,0)
local bitangent = Vector(0,0,0)
tangent.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x)
tangent.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y)
tangent.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z)
bitangent.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
bitangent.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
bitangent.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
local binormal = bitangent:GetNormalized()
tangent = Vector(1,0,0)
binormal = Vector(0,1,0)
--bitangent = bitangent:GetNormalized()
triangle[ i ].tangent = tangent
triangle[ i + 1 ].tangent =tangent
triangle[ i + 2 ].tangent = tangent
triangle[ i ].binormal = binormal
triangle[ i + 1 ].binormal = binormal
triangle[ i + 2 ].binormal = binormal
local udata = {0,0,1,1}
triangle[ i ].userdata = udata
triangle[ i + 1 ].userdata = udata
triangle[ i + 2 ].userdata = udata
end
end
--- Returns all original faces. ( Warning, uses a lot of memory )
--- @return OriginalFace[]
function meta:GetOriginalFaces()
if self._originalfaces then return self._originalfaces end
--- @class OriginalFace[]
self._originalfaces = {}
local data = self:GetLump( 27 )
for i = 1, math.min( data:Size() / 448, MAX_MAP_FACES ) do
--- @class OriginalFace
--- @field styles number[]
--- @LightmapTextureMinsInLuxels number[]
--- @LightmapTextureSizeInLuxels number[]
local t = {}
t.plane = self:GetPlanes()[data:ReadUShort()]
t.side = data:ReadByte()
t.onNode = data:ReadByte()
t.firstedge = data:ReadLong()
t.numedges = data:ReadShort()
t.texinfo = data:ReadShort()
t.dispinfo = data:ReadShort()
t.surfaceFogVolumeID = data:ReadShort()
t.styles = { data:ReadByte(), data:ReadByte(), data:ReadByte(), data:ReadByte() }
t.lightofs = data:ReadLong()
t.area = data:ReadFloat()
t.LightmapTextureMinsInLuxels = { data:ReadLong(), data:ReadLong() }
t.LightmapTextureSizeInLuxels = { data:ReadLong(), data:ReadLong() }
t.origFace = data:ReadLong()
t.numPrims = data:ReadUShort()
t.firstPrimID = data:ReadUShort()
t.smoothingGroups = data:ReadULong()
t.__map = self
t.__id = i
self._originalfaces[i] = t
end
self:ClearLump( 27 )
return self._originalfaces
end
end
-- Model The brush-models embedded within the map. 0 is always the entire map.
do
--- @class BModel
local meta_bmodel = {}
meta_bmodel.__index = meta_bmodel
--- Returns a list of BModels ( Brush Models )
--- @return BModel
function meta:GetBModels()
if self._bmodel then return self._bmodel end
self._bmodel = {}
local data = self:GetLump( 14 )
for i = 0, data:Size() / 384 - 1 do
--- @class BModel
local t = {}
t.mins = data:ReadVector()
t.maxs = data:ReadVector()
t.origin = data:ReadVector()
t.headnode = data:ReadLong()
t.firstface = data:ReadLong()
t.numfaces = data:ReadLong()
t.__map = self
setmetatable( t, meta_bmodel )
self._bmodel[i] = t
end
self:ClearLump( 14 )
return self._bmodel
end
--- Returns a list of Faces making up this bmodel
--- @return FaceObject[]
function meta_bmodel:GetFaces()
local t = {}
local c = 1
local faces = self.__map:GetFaces()
for i = self.firstface, self.firstface + self.numfaces - 1 do
t[c] = faces[i]
c = c + 1
end
return t
end
--- Locates the BModelIndex for the said faceIndex
--- @param faceId number
--- @return number
function meta:FindBModelIDByFaceIndex( faceId )
local bModels = self:GetBModels()
for i = 0, #bModels do
local q = bModels[i]
if q.numfaces >= 0 and faceId >= q.firstface and faceId < q.firstface + q.numfaces then
return i
end
end
return 0
end
end
-- Special custom functions
do
local lower = string.lower
--- Returns true if the position is outside the map
--- @param position Vector
--- @return boolean
function meta:IsOutsideMap( position )
local leaf = self:PointInLeaf( 0, position )
if not leaf then return true end -- No leaf? Shouldn't be possible.
return leaf:IsOutsideMap()
end
--- Returns a list of all materials used by the map.
--- @return IMaterial[]
function meta:GetMaterials()
if self._materials then return self._materials end
--- @type IMaterial[]
self._materials = {}
for _, v in pairs( self:GetTextures() ) do
if v then
local m = Material( v )
if m then table.insert( self._materials, m ) end
end
end
return self._materials
end
--- Returns true if the texture is used by the map.
--- @param texture string
--- @return boolean
function meta:HasTexture( texture )
texture = lower( texture )
for _, v in pairs( self:GetTextures() ) do
if v and lower( v ) == texture then
return true
end
end
return false
end
--- Returns true if the material is used by the map.
--- @param material IMaterial
--- @return boolean
function meta:HasMaterial( material )
return self:HasTexture( material:GetName() )
end
--- Returns true if the skybox is rendering at this position.
--- Note: Seems to be broken in EP2 and beyond, where the skybox is rendered at all times regardless of position.
--- @return boolean
function meta:IsRenderingSkyboxAtPosition( position )
local leaf = self:PointInLeaf( 0, position )
if not leaf then return false end -- No leaf? Shouldn't be possible
return leaf:Has2DSkyboxInPVS()
end
--- Returns a list of skybox leafs (If the map has a skybox)
--- @return LeafObject[]
function meta:GetSkyboxLeafs()
if self._skyboxleafs then return self._skyboxleafs end
--- @type LeafObject[]
self._skyboxleafs = {}
local t = self:FindByClass( "sky_camera" )
if #t < 1 then return self._skyboxleafs end -- Unable to locate skybox leafs
local p = t[1].origin
local leaf = self:PointInLeaf( 0, p )
if not leaf then return self._skyboxleafs end
local area = leaf.area
local i = 1
for _, l in ipairs( self:GetLeafs() ) do
if l.area == area then
self._skyboxleafs[i] = l
i = i + 1
end
end
return self._skyboxleafs
end
--- Returns the size of the skybox
--- @return Vector|nil
--- @return Vector|nil
function meta:GetSkyboxSize()
if self._skyboxmin and self._skyboxmaxs then return self._skyboxmin, self._skyboxmaxs end
for _, leaf in ipairs( self:GetSkyboxLeafs() ) do
if not self._skyboxmin then
self._skyboxmin = Vector( leaf.mins )
else
self._skyboxmin.x = math.min( self._skyboxmin.x, leaf.mins.x )
self._skyboxmin.y = math.min( self._skyboxmin.y, leaf.mins.y )
self._skyboxmin.z = math.min( self._skyboxmin.z, leaf.mins.z )
end
if not self._skyboxmaxs then
self._skyboxmaxs = Vector( leaf.maxs )
else
self._skyboxmaxs.x = math.max( self._skyboxmaxs.x, leaf.maxs.x )
self._skyboxmaxs.y = math.max( self._skyboxmaxs.y, leaf.maxs.y )
self._skyboxmaxs.z = math.max( self._skyboxmaxs.z, leaf.maxs.z )
end
end
return self._skyboxmin, self._skyboxmaxs
end
end
-- Old debug code below
if true then return end
function TESTVPS()
--map:PVSForOrigin(Vector(0,0,0)) -- Warmup
local vec = LocalPlayer():GetPos()
local s = SysTime()
local map = NikNaks.Map()
local vec = map:PVSForOrigin(vec)
print(string.format("%fs", SysTime() - s))
end
local cTrace, i = nil, 0
local oldLeaf
local matList = {}
PVS_DEBUG = false
hook.Add("PostDrawOpaqueRenderables", "TEST2", function(a, b)
if a then return end
local leaf, new = NikNaks.CurrentMap:PointInLeafCache(0, LocalPlayer():GetShootPos(), oldLeaf)
--if not leaf then return end
--leaf:DebugRender()
if a or not PVS_DEBUG then return end
--for id, leaf in pairs( NikNaks.CurrentMap:GetLeafClusters()[0] ) do
-- leaf:DebugRender()
--end
local leaf, new = NikNaks.CurrentMap:PointInLeafCache(0, LocalPlayer():GetShootPos(), oldLeaf)
oldLeaf = leaf
if new then
print(string.format("Leaf %i, Area %i, Cluster %i, Flags %i", leaf.__id, leaf.area, leaf.cluster, leaf.flags))
end
local PVS = NikNaks.CurrentMap:PVSForOrigin( EyePos())
for _, leaf in pairs( PVS:GetLeafs() ) do
if type(leaf) == "number" then continue end
leaf:DebugRender( leaf:Has2DSkyInPVS() and Color(0,0,255) or leaf:Has3DSkyInPVS() and Color(255,0,0) or Color(0,255,0))
end
--for id, leaf2 in pairs( NikNaks.CurrentMap:GetLeafs() ) do
-- if type(leaf2) == "number" then continue end
-- if leaf2.area ~= leaf.area then continue end
-- leaf2:DebugRender()
--end
end)
hook.Add("PostDrawOpaqueRenderables", "TEST", function()
if not NikNaks or not NikNaks.CurrentMap then return end
if true then return end
i = i + 1
local a, b = Entity(497):GetPos(), Entity(498):GetPos()
if i > 10 then
cTrace = NikNaks.CurrentMap:TestTraceLeaf(a, b)
i = 0
end
if not cTrace then return end
--render.DrawLine( a, b, color_white, true )
render.DrawLine( a, cTrace.hitpos, cTrace.hit and Color(0,255,0) or color_white, true )
render.SetMaterial(mat)
render.DrawQuadEasy( cTrace.hitpos, cTrace.hitnormal, 64, 64, color_white, (CurTime() * -20) % 360 )
debugRender(cTrace)
end)
local mat = Material("effects/wheel_ring")
local cMat = Material("vgui/avatar_default")
local qMat = cMat
local lMat
local function GetMat( str )
if lMat and lMat == str then return cMat end
lMat = str
cMat = Material(str)
return cMat
end
local function debugRender( trace )
local angle = EyeAngles()
-- Only use the Yaw component of the angle
angle = Angle( 0, angle.y - 90, 90 )
local mat = cMat
if trace.surface and trace.surface.nameStringTableID then
mat = GetMat(trace.surface.nameStringTableID)
end
cam.Start3D2D( trace.hitpos, angle, .5 )
draw.SimpleText( "Fraction: " .. trace.fraction, "Default", 0, 0, color_white )
draw.SimpleText( "Hit: " .. tostring(trace.hit), "Default", 0, 14, color_white )
cam.End3D2D()
if not trace.hit then return end
qMat:SetTexture("$basetexture", mat:GetTexture("$basetexture"))
render.SetMaterial(qMat)
render.DrawQuadEasy(trace.hitpos + Vector(0,0,32) -EyeAngles():Forward() * 10, -EyeAngles():Forward(), 32, 32, color_white, 180)
qMat:SetTexture("$basetexture", "effects/wheel_ring")
end
local function emptyTrace()
return {
mins = Vector(0,0,0),
maxs = Vector(0,0,0),
extents = Vector(0,0,0)
}
end
local NEVER_UPDATED = -9999
local DIST_EPSILON = 0.03125
function DM_ClipBoxToBrush(trace, mins, maxs, p1, p2, self)
if self.numsides < 1 then return end
local ofs = Vector(0,0,0)
local dist = 0
local enterfrac = NEVER_UPDATED
local leavefrac = 1
local clipplane
local getout = false
local startout = false
local leadside = nil
if not trace.ispoint then
for i = 1, self.numsides - 1 do
local side = self.sides[i]
local plane = side.plane
ofs.x = plane.normal.x < 0 and maxs.x or mins.x
ofs.y = plane.normal.y < 0 and maxs.y or mins.y
ofs.z = plane.normal.z < 0 and maxs.z or mins.z
dist = ofs:Dot( plane.normal )
dist = plane.dist - dist
d1 = p1:Dot( plane.normal ) - dist
d2 = p2:Dot( plane.normal ) - dist
if d1 > 0 and d2 > 0 then return end
if d2 > 0 then getout = true end
if d1 > 0 then startout = true end
if d1 <= 0 and d2 <= 0 then continue end
if d1 > d2 then
local f = (d1 - DIST_EPSILON) / (d1 - d2)
if f > enterfrac then
enterfrac = f
clipplane = plane
leadside = side
end
else
f = (d1+DIST_EPSILON) / (d1 - d2)
if f < leavefrac then leavefrac = f end
end
end
else
for i = 1, self.numsides - 1 do
local side = self.sides[i]
local plane = side.plane
local texinfo = self.__map:GetTexInfo()[ side.texinfo ]
local surfaces = self.__map:GetTexData()[ texinfo.texdata ]
--if surfaces and surfaces.nameStringTableID == "TOOLS/TOOLSNODRAW" then continue end
if side.bevel == 1 then continue end
dist = plane.dist
d1 = p1:Dot(plane.normal) - dist
d2 = p2:Dot(plane.normal) - dist
if d1 > 0 and d2 > 0 then return end
if d2 > 0 then getout = true end
if d1 > 0 then startout = true end
if d1 < 0 and d2 < 0 then continue end
if d1 > d2 then
f = (d1 - DIST_EPSILON) / (d1-d2)
if f > enterfrac then
enterfrac = f
clipplane = plane
leadside = side
end
else
f = (d1+DIST_EPSILON) / (d1-d2)
if f < leavefrac then leavefrac = f end
end
end
end
if not startout then
trace.startspolid = true
if not getout then
trace.allsolid = true
end
return
end
if enterfrac < leavefrac then
if enterfrac > NEVER_UPDATED and enterfrac < trace.fraction then
if enterfrac < 0 then
enterfrac = 0
end
trace.fraction = enterfrac
trace.plane = clipplane
trace.type = clipplane.type
if leadside.texinfo ~= -1 then
local texinfo = self.__map:GetTexInfo()[ leadside.texinfo ]
trace.surface = self.__map:GetTexData()[ texinfo.texdata ]
else
trace.surface = 0
end
trace.side = leadside
trace.contents = self.contents
end
end
return trace
end
function meta:TestTraceLeaf(from, to)
local leaf = self:PointInLeaf(0, from)
local flb = leaf.firstleafbrush
local trace = emptyTrace()
trace.startsolid = false
trace.fraction = 1
trace.ispoint = true
trace.hitnormal = Vector(0,0,0)
for i = 0, leaf.numleafbrushes - 1 do
local v = self:GetLeafBrushes()[flb + i]
--If brush is solid
if bit.band(v.contents, 1) == 0 then continue end
DM_ClipBoxToBrush( trace, trace.mins, trace.maxs, from, to, v)
if trace.fraction ~= 1 or trace.startsolid then
if trace.startsolid then
trace.fraction = 0
end
trace.hit = true
trace.hitnormal = trace.plane.normal
trace.hitpos = from + (to - from) * trace.fraction
return trace
end
end
trace.hitpos = to
trace.hit = false
return trace
end
function meta:TestTrace(from, to)
local trace = emptyTrace()
trace.startsolid = false
trace.fraction = 1
trace.ispoint = false
trace.hitnormal = Vector(0,0,0)
for k, v in pairs( self:GetBrushes() ) do
--local v = self:GetLeafBrushes()[flb + i]
--If brush is solid
if bit.band(v.contents, 1) == 0 then continue end
DM_ClipBoxToBrush( trace, trace.mins, trace.maxs, from, to, v)
if trace.fraction ~= 1 or trace.startsolid then
if trace.startsolid then
trace.fraction = 0
end
trace.hit = true
trace.hitnormal = trace.plane.normal
trace.hitpos = from + (to - from) * trace.fraction
return trace
end
end
trace.hitpos = to
trace.hit = false
return trace
end
-- Can't load these maps normally
local brokenMaps = {
["d2_coast_02.bsp"] = true,
["ep1_citadel_00_demo.bsp"] = true
}
local function assertMap( BSP )
-- Check Entity
local worldent = BSP:GetEntities()[0]
assert(worldent.classname == "worldspawn", "Invalid MapEntity!")
-- Check leaf
local leaf = BSP:GetLeafs()[0]
assert(leaf.numleaffaces == 0, "Invalid Leaf")
assert(leaf.leafWaterDataID == -1, "Invalid Leaf")
assert(leaf.maxs.x == 0, "Invalid Leaf")
-- Check face
local face = BSP:GetFaces()[0]
assert(face, "Invalid Face")
BSP:GetTextures()
end
function NikNaks.MapDebug( areYouSure )
if not areYouSure or areYouSure ~= "iamsure" then return end
print("Scanning all maps for any parse / data errors")
local t = file.Find("maps/*.BSP", "GAME")
local list = {}
for _, mapString in pairs( t ) do
if brokenMaps[mapString] then continue end
table.insert(list, mapString)
end
local maps = #list
local thread = coroutine.create(function()
while true do
if #list < 1 then coroutine.yield(true) return end
local mapname = table.remove(list, 1)
print("> Testing", mapname)
local s = SysTime()
local BSP = NikNaks.Map( mapname )
assert( BSP, "Unable to parse MAP" )
assertMap( BSP )
print(string.format("%s took %fs", string.NiceSize(BSP._size), SysTime() - s))
coroutine.wait( 0.1 )
end
end)
hook.Add("Think", "NikNaks.BSPDEBUG", function()
local noerr, yield = coroutine.resume( thread )
if yield or not noerr then
print(string.format("Scanned %i maps.", maps))
hook.Remove("Think", "NikNaks.BSPDEBUG")
end
end)
end