mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
1650 lines
43 KiB
Lua
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
|