This commit is contained in:
lifestorm
2024-08-04 23:54:45 +03:00
parent 8064ba84d8
commit 6a58f406b1
7522 changed files with 4011896 additions and 15 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/
-- All Rights Reserved. Not allowed to be reuploaded.
-- License: https://github.com/Nak2/NikNaks/blob/main/LICENSE
local obj_tostring = "BSP %s [ %s ]"
local format = string.format
--- @class BSPObject
local meta = NikNaks.__metatables["BSP"]
--- @class BrushObject
local meta_brush = {}
meta_brush.__index = meta_brush
meta_brush.__tostring = function( self ) return format( obj_tostring, "BSP Brush", self.__id ) end
meta_brush.MetaName = "BSP Brush"
NikNaks.__metatables["BSP Brush"] = meta_brush
local DIST_EPSILON = 0.03125
local MAX_MAP_BRUSHES = 8192
local MAX_MAP_BRUSHSIDES = 65536
--- Returns an array of the brush-data with brush-sides.
--- @return BrushObject[]
function meta:GetBrushes()
if self._brushes then return self._brushes end
self._brushes = {}
local data = self:GetLump( 18 )
for id = 1, math.min( data:Size() / 96, MAX_MAP_BRUSHES ) do
--- @class BrushObject
local t = {}
local first = data:ReadLong()
local num = data:ReadLong()
t.contents = data:ReadLong()
t.numsides = num
--- @type BrushSideObject[]
t.sides = {}
t.__id = id
t.__map = self
local n = 1
for i = first, first + num - 1 do
t.sides[n] = self:GetBrushSides()[i]
n = n + 1
end
self._brushes[id] = setmetatable( t, meta_brush )
end
self:ClearLump( 18 )
return self._brushes
end
--- Returns an array of brushside-data.
--- @return BrushSideObject[]
function meta:GetBrushSides()
if self._brushside then return self._brushside end
self._brushside = {}
local data = self:GetLump( 19 )
local planes = self:GetPlanes()
for i = 1, math.min( data:Size() / 64, MAX_MAP_BRUSHSIDES ) do
--- @class BrushSideObject
local t = {}
t.plane = planes[ data:ReadUShort() ]
t.texinfo = data:ReadShort()
t.dispinfo = data:ReadShort()
local q = data:ReadShort()
t.bevel = bit.band( q, 0x1 ) == 1 -- Seems to be 1 if used for collision detection
t.thin = bit.rshift( q, 8 ) == 1 -- For Portal 2 / Alien Swarm
self._brushside[i - 1] = t
end
self:ClearLump( 19 )
return self._brushside
end
function meta_brush:GetIndex()
return self.__id or -1
end
--- Returns the content flag the brush has.
--- @return number
function meta_brush:GetContents()
return self.contents
end
--- Returns true if the brush has said content
--- @param CONTENTS number
--- @return boolean
function meta_brush:HasContents( CONTENTS )
if CONTENTS == 0 then return self.contents == CONTENTS end
return bit.band( self.contents, CONTENTS ) ~= 0
end
-- Texture Stuff
--- Returns the TexInfo for the brush-side.
--- @param side number
--- @return table
function meta_brush:GetTexInfo( side )
return self.__map:GetTexInfo()[self.sides[side].texinfo]
end
--- Returns the TexData for the brush-side.
--- @param side number
--- @return table
function meta_brush:GetTexData( side )
return self.__map:GetTexData()[ self:GetTexInfo( side ).texdata]
end
--- Returns the texture for the brush-side.
--- @param side number
--- @return string
function meta_brush:GetTexture( side )
local t = self:GetTexData( side ) or {}
return t.nameStringTableID
end
--- Returns the Material for the brush-side.
--- @param side number
--- @return IMaterial
function meta_brush:GetMaterial( side )
if self._material and self._material[side] then return self._material[side] end
if not self._material then self._material = {} end
self._material[side] = Material( self:GetTexture( side ) or "__error" )
return self._material[side]
end
--- Returns true if the point is inside the brush
--- @param position Vector
--- @return boolean
function meta_brush:IsPointInside( position )
for i = 1, self.numsides do
local side = self.sides[i]
local plane = side.plane
if plane.normal:Dot( position ) - plane.dist > DIST_EPSILON then
return false
end
end
return true
end

View File

@@ -0,0 +1,250 @@
--[[
| 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
-- Entities are stored in a KeyValues table.
-- However we can't use the KeyValuesToTablePreserveOrder function, since some BSPs have errors within Entity Lump.
--- @class EntityObject
--- @field origin? Vector
--- @field angles? Angle
--- @field rendercolor? Color
--- @field ontrigger? table
--- @field classname? string
--- @field model? string
--- @field targetname? string
--- @field world_mins? string
--- @field world_maxs? string
--- @field scale? number
--- @field coldworld? number
--- Locates the next enter-token
--- @param data string
--- @param pos number
--- @return number
local function findNextToken( data, pos )
for i = pos, #data do
if data[i] == "{" then return i end
end
return -1
end
--- Locates the next exit-token
local function findNextExitToken( data, pos )
local keypos = 0
local ignore = false
for i = pos, #data do
if data[i] == "\"" then ignore = not ignore
elseif ignore then continue end
if data[i] == "{" then keypos = keypos + 1
elseif data[i] == "}" then
keypos = keypos - 1
if keypos == 0 then return i end
end
end
end
--- Convert a few things to make it easier to read entities.
--- @param t EntityObject
local function postEntParse( t )
t.origin = util.StringToType( t.origin or "0 0 0", "Vector" )
t.angles = util.StringToType( t.angles or "0 0 0", "Angle" )
if t.rendercolor then
local c = util.StringToType( t.rendercolor or "255 255 255", "Vector" )
t.rendercolor = Color( c.x, c.y, c.z, 255 )
end
-- Make sure ontrigger is a table.
if t.ontrigger and type( t.ontrigger ) ~= "table" then
t.ontrigger = { t.ontrigger }
end
end
-- A list of data-keys that can have multiple entries.
local _tableTypes = {
["OnMapSpawn"] = true,
["OnTrigger"] = true,
["OnStartTouch"] = true,
["OnArrivedAtDestinationNode"] = true,
["OnPowered"] = true,
["OnUnpowered"] = true,
["OnExplode"] = true,
["OnAllTrue"] = true,
}
--- @return EntityObject
local function ParseEntity( str )
--- @class EntityObject
local t = {}
for key, value in string.gmatch( str, [["(.-)".-"(.-)"]] ) do
value = tonumber( value ) or value
if t[key] then
if type( t[key] ) ~= "table" then
t[key] = { t[key] }
else
table.insert( t[key], value )
end
elseif _tableTypes[key] then
t[key] = { value }
else
t[key] = value
end
end
postEntParse( t )
return t
end
--- Tries to parse the entity-data.
--- @param data string
--- @return EntityObject[]
local function parseEntityData( data )
-- Cut the data into bits
local charPos = 1
local tabData = {}
for _ = 1, #data do -- while true do
local nextToken = findNextToken( data, charPos )
if nextToken < 0 then
break -- No token found. EOF.
else
local exitToken = findNextExitToken( data, nextToken )
if exitToken then
tabData[#tabData + 1] = data:sub( nextToken, exitToken )
charPos = exitToken
else -- ERROR No exit token? Try and parse the rest.
tabData[#tabData + 1] = data:sub( nextToken ) .. "}"
NikNaks.Msg( [[[BSP] ParseEntity: No closing brace found!]] )
break
end
end
end
local tab = {}
for id, str in pairs( tabData ) do
local t = ParseEntity( str )
tab[id - 1] = t
end
return tab
end
--- @class BSPObject
local meta = NikNaks.__metatables["BSP"]
--- Returns a list of all raw-entity data within the BSP.
--- @return EntityObject[]
function meta:GetEntities()
if self._entities then return self._entities end
-- Since it is stringbased, it is best to keep it as a string.
local data = self:GetLumpString( 0 )
-- Parse all entities
self._entities = parseEntityData( data )
return self._entities
end
--- Returns the raw entity data said entity.
--- @param index number
--- @return EntityObject
function meta:GetEntity( index )
return self:GetEntities()[index]
end
--- Returns a list of entity data, matching the class.
--- @param class string
--- @return EntityObject[]
function meta:FindByClass( class )
local t = {}
for _, v in pairs( self:GetEntities() ) do
local vClass = v.classname
if class and string.match( vClass, class ) then
t[#t + 1] = v
end
end
return t
end
--- Returns a list of entity data, matching the model.
--- @param model string
--- @return EntityObject[]
function meta:FindByModel( model )
local t = {}
for _, v in pairs( self:GetEntities() ) do
if v.model == model then
t[#t + 1] = v
end
end
return t
end
--- Returns a list of entity data, matching the name ( targetname ).
--- @param name string
--- @return table
function meta:FindByName( name )
local t = {}
for _, v in pairs( self:GetEntities() ) do
if v.targetname == name then
t[#t + 1] = v
end
end
return t
end
--- Returns a list of entity data, within the specified box. Note: This (I think) is slower than ents.FindInBox
--- @param boxMins Vector
--- @param boxMaxs Vector
--- @return table
function meta:FindInBox( boxMins, boxMaxs )
local t = {}
for _, v in pairs( self:GetEntities() ) do
local origin = v.origin
if origin and v.origin:WithinAABox( boxMins, boxMaxs ) then
t[#t + 1] = v
end
end
return t
end
--- Returns a list of entity data, within the specified sphere. Note: This (I think) is slower than ents.FindInSphere
--- @param origin Vector
--- @param radius number
function meta:FindInSphere( origin, radius )
radius = radius ^ 2
local t = {}
for _, v in pairs( self:GetEntities() ) do
local vOrigin = v.origin
if vOrigin and vOrigin:DistToSqr( origin ) <= radius then
t[#t + 1] = v
end
end
return t
end

View File

@@ -0,0 +1,444 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/
-- All Rights Reserved. Not allowed to be reuploaded.
-- License: https://github.com/Nak2/NikNaks/blob/main/LICENSE
local obj_tostring = "BSP %s [ %s ]"
local format = string.format
--- @class BSPObject
local meta = NikNaks.__metatables["BSP"]
--- @class FaceObject
local meta_face = {}
meta_face.__index = meta_face
meta_face.__tostring = function( self ) return format( obj_tostring, "Faces", self.__id ) end
meta_face.MetaName = "BSP Faces"
NikNaks.__metatables["BSP Faces"] = meta_face
local MAX_MAP_FACES = 65536
--- Returns all faces. ( Warning, uses a lot of memory )
--- @return FaceObject[]
function meta:GetFaces()
if self._faces then return self._faces end
self._faces = {}
local data = self:GetLump( 7 )
for i = 0, math.min( data:Size() / 448, MAX_MAP_FACES ) - 1 do
--- @class FaceObject
local t = {}
t.planenum = data:ReadUShort()
t.plane = self:GetPlanes()[ t.planenum ]
t.side = data:ReadByte() -- 1 = same direciton as face
t.onNode = data:ReadByte() -- 1 if on node, 0 if in leaf
t.firstedge = data:ReadLong()
t.numedges = data:ReadShort()
t.texinfo = data:ReadShort() -- Texture info
t.dispinfo = data:ReadShort() -- Displacement info
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.__bmodel = self:FindBModelIDByFaceIndex( i )
t.__map = self
t.__id = i
setmetatable( t, meta_face )
self._faces[i] = t
end
self:ClearLump( 7 )
return self._faces
end
-- Returns the original face
function meta:GetOriginalFace()
return self.__map:GetOriginalFaces()[self.origFace]
end
-- We make a small hack to cache and get the entities using brush-models.
local function __findEntityUsingBrush( self )
if self.__funcBrush then return self.__funcBrush end
local entities = self:GetEntities()
self.__funcBrush = { [0] = entities[0] }
for _, v in pairs( entities ) do
local numMdl = string.match( v.model or "", "*([%d]+)" )
if numMdl then
self.__funcBrush[tonumber( numMdl )] = v
end
end
return self.__funcBrush
end
--- Parses a Color object from the given BitBuffer
--- @param data BitBuffer
--- @return Color
local function __readColorRGBExp32 ( data )
return NikNaks.ColorRGBExp32ToColor( {
r = data:ReadByte(),
g = data:ReadByte(),
b = data:ReadByte(),
exponent = data:ReadSignedByte()
} )
end
--- Returns the lightmap samples for the face.
--- @return table<string, LightmapSample[]>?
function meta_face:GetLightmapSamples()
local lightofs = self.lightofs
if lightofs == -1 then return end
if self._lightmap_samples then return self._lightmap_samples end
--- @class LightmapSample
--- @field color Color
--- @field exponent number
--- @type LightmapSample[]
local full = {}
--- @type LightmapSample[]
local average = {}
local samples = { average = average, full = full }
self._lightmap_samples = samples
local has_bumpmap = self:GetMaterial():GetString( "$bumpmap" ) ~= nil
local luxel_count = ( self.LightmapTextureSizeInLuxels[1] + 1 ) * ( self.LightmapTextureSizeInLuxels[2] + 1 )
local lightstyle_count = 0
for _, v in ipairs( self.styles ) do
if v ~= 255 then lightstyle_count = lightstyle_count + 1 end
end
-- "For faces with bumpmapped textures, there are four times the usual number of lightmap samples"
local sample_count = lightstyle_count * luxel_count
if has_bumpmap then sample_count = sample_count * 4 end
local data = self.__map:GetLump( 8 )
-- Get the average samples
-- "Immediately preceeding the lightofs-referenced sample group,
-- there are single samples containing the average lighting on the face, one for each lightstyle,
-- in reverse order from that given in the styles[] array."
local color, exponent
data:Seek( ( lightofs * 8 ) - ( 32 * lightstyle_count ) )
for _ = 1, lightstyle_count do
color, exponent = __readColorRGBExp32( data )
table.insert( average, 1, { color = color, exponent = exponent } )
end
-- Get the full samples
for _ = 1, sample_count do
color, exponent = __readColorRGBExp32( data )
table.insert( full, { color = color, exponent = exponent } )
end
return samples
end
--- Returns the face-index.
--- @return number
function meta_face:GetIndex()
return self.__id or -1
end
--- Returns the normal for the face
--- @return Vector
function meta_face:GetNormal()
return self.plane.normal
end
--- Returns the texture info for the face.
--- @return table
function meta_face:GetTexInfo()
return self.__map:GetTexInfo()[self.texinfo]
end
--- Returns the texture data for the face.
--- @return table
function meta_face:GetTexData()
return self.__map:GetTexData()[ self:GetTexInfo().texdata ]
end
--- Returns the texture for the face.
function meta_face:GetTexture()
return self:GetTexData().nameStringTableID
end
--- Returns the material the face use. Note: Materials within the BSP is not loaded.
--- @return IMaterial
function meta_face:GetMaterial()
if self._mat then return self._mat end
self._mat = Material( self:GetTexture() or "__error" )
return self._mat
end
--- Returns true if the face should render.
--- @return boolean
function meta_face:ShouldRender()
local texinfo = self:GetTexInfo()
local flags = texinfo and texinfo.flags or 0
return bit.band( flags, 0x80 ) == 0 and bit.band( flags, 0x200 ) == 0
end
--- Returns true if the face-texture is translucent
--- @return boolean
function meta_face:IsTranslucent()
local texinfo = self:GetTexInfo() or 0
return bit.band( texinfo.flags, 0x10 ) ~= 0
end
--- Returns true if the face is part of 2D skybox.
--- @return boolean
function meta_face:IsSkyBox()
local texinfo = self:GetTexInfo() or 0
return bit.band( texinfo.flags, 0x2 ) ~= 0
end
--- Returns true if the face is part of 3D skybox.
--- @return boolean
function meta_face:IsSkyBox3D()
local texinfo = self:GetTexInfo() or 0
return bit.band( texinfo.flags, 0x4 ) ~= 0
end
--- Returns true if the face's texinfo has said flag.
--- @return boolean
function meta_face:HasTexInfoFlag( flag )
local texinfo = self:GetTexInfo() or 0
return bit.band( texinfo.flags, flag ) ~= 0
end
--- Returns true if the face is part of the world and not another entity.
--- @return boolean
function meta_face:IsWorld()
return self.__bmodel == 0
end
--- Returns the BModel the face has. 0 if it is part of the world.
--- @return number
function meta_face:GetBModel()
return self.__bmodel
end
--- Returns the entity-object-data that is part of this face.
--- @return string EntityData
function meta_face:GetEntity()
return __findEntityUsingBrush( self.__map )[self.__bmodel]
end
-- Displacments TODO: Fix Displacment Position and Data
--- Returns true if the face is part of Displacment
--- @return boolean
function meta_face:IsDisplacment()
return self.dispinfo > -1
end
--- Returns the vertex positions for the face. [Not Cached]
--- Note this will ignore BModel-positions!
--- @return table
function meta_face:GetVertexs()
if self._vertex then return self._vertex end
local t = {}
if not self:IsDisplacment() then
for i = 0, self.numedges - 1 do
t[i + 1] = self.__map:GetSurfEdgesIndex( self.firstedge + i )
end
return t
end
-- This is a displacment
-- TODO: Calculate the displacment mesh and return it here
--local dispVertStart =
end
--- Returns a table in form of a polygon-mesh. [Not Cached]
--- @return PolygonMeshVertex[]
function meta_face:GenerateVertexData()
--- @type PolygonMeshVertex[]
local t = {}
local tv = self:GetTexInfo().textureVects
local lv = self:GetTexInfo().lightmapVecs
local texdata = self:GetTexData()
local mat_w, mat_h = texdata.view_width, texdata.view_height
local n = self:GetNormal()
-- Move the faces to match func_brushes (If any)
local bNum = self.__bmodel
local exPos, exAng
if bNum > 0 then
-- Get funch_brushes and their location
local func_brush = __findEntityUsingBrush( self.__map )[bNum]
if func_brush then
exPos = func_brush.origin
exAng = func_brush.angles
end
end
local luxelW = self.LightmapTextureSizeInLuxels[1] + 1
local luxelH = self.LightmapTextureSizeInLuxels[2] + 1
for i = 0, self.numedges - 1 do
--- @class PolygonMeshVertex
local vert = {}
local a = self.__map:GetSurfEdgesIndex( self.firstedge + i )
vert.pos = a
if bNum > 0 then -- WorldPos -> Entity Brush
a = WorldToLocal( a, Angle( 0, 0, 0 ), Vector( 0, 0, 0 ), exAng )
vert.pos = a + exPos
end
vert.normal = n
-- UV & LV
vert.u = ( tv[0][0] * a.x + tv[0][1] * a.y + tv[0][2] * a.z + tv[0][3] ) / mat_w
vert.v = ( tv[1][0] * a.x + tv[1][1] * a.y + tv[1][2] * a.z + tv[1][3] ) / mat_h
vert.lu = ( ( lv[0][0] * a.x + lv[0][1] * a.y + lv[0][2] * a.z + lv[0][3] ) - self.LightmapTextureMinsInLuxels[1] ) / luxelW
vert.lv = ( ( lv[1][0] * a.x + lv[1][1] * a.y + lv[1][2] * a.z + lv[1][3] ) - self.LightmapTextureMinsInLuxels[2] ) / luxelH
vert.userdata = { 0, 0, 0, 0 } -- Todo: Calculate this?
t[i + 1] = vert
end
return t
end
--- @return PolygonMeshVertex[]?
local function PolyChop( o_vert )
local vert = {}
if #o_vert < 3 then return end
local n = 1
local triCount = #o_vert - 2
for i = 1, triCount do
vert[n] = o_vert[1]
vert[n + 1] = o_vert[i + 1]
vert[n + 2] = o_vert[i + 2]
n = n + 3
end
return vert
end
---Returns a table in form of a polygon-mesh for triangles. [Not Cached]
---@return PolygonMeshVertex[]?
function meta_face:GenerateVertexTriangleData()
if self._vertTriangleData then return self._vertTriangleData end
self._vertTriangleData = PolyChop( self:GenerateVertexData() )
return self._vertTriangleData
end
--- All mesh-data regarding said face. Should use face:GenerateVertexTriangleData intead!
--- @return table
function meta_face:GenerateMeshData()
--- @class PolygonMeshData
local t = {}
t.verticies = self:GenerateVertexData()
t.triangles = PolyChop( t.verticies )
t.material = self:GetTexture()
return {t}
end
if CLIENT then
--- @type IMesh[]
NIKNAKS_TABOMESH = NIKNAKS_TABOMESH or {}
--- Builds the mesh if face has none.
--- @return IMesh|boolean?
function meta_face:BuildMesh()
if SERVER then return end
if self._mesh then return self._mesh end
-- Tex
local texinfo = self:GetTexInfo()
if bit.band( texinfo.flags, 0x80 ) ~= 0 or bit.band( texinfo.flags, 0x200 ) ~= 0 then
self._mesh = false
return self._mesh
end
local meshData = self:GenerateVertexTriangleData()
if not meshData then return self._mesh end
self._mesh = Mesh( self:GetMaterial() )
-- Vert
mesh.Begin( self._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.Color(col.r, col.g, col.b, col.a)
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()
table.insert( NIKNAKS_TABOMESH, self._mesh )
return self._mesh
end
--- Returns the mesh generated for the face.
--- Note. Need to call face:BuildMesh first.
--- @return IMesh|boolean?
function meta_face:GetMesh()
return self._mesh
end
--- Deletes the mesh generated for the face.
--- @return self
function meta_face:DeleteMesh()
if not self._mesh then return end
self._mesh:Destroy()
self._mesh = nil
return self
end
--- Generates a mesh for the face and renders it.
--- @param dontGenerate boolean
--- @return boolean? didGenerateMesh
function meta_face:DebugRender( dontGenerate )
local _mesh = self:GetMesh()
if not _mesh and dontGenerate then return false end
_mesh = _mesh or self:BuildMesh()
if not IsValid( _mesh ) then return end
render.SetMaterial( self:GetMaterial() )
_mesh:Draw()
return true
end
for _, _mesh in pairs( NIKNAKS_TABOMESH ) do
if IsValid( _mesh ) then _mesh:Destroy() end
end
end

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,284 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/
-- All Rights Reserved. Not allowed to be reuploaded.
-- License: https://github.com/Nak2/NikNaks/blob/main/LICENSE
local obj_tostring = "BSP %s"
local format = string.format
--- @class BSPObject
local meta = NikNaks.__metatables["BSP"]
local meta_leaf = NikNaks.__metatables["BSP Leaf"]
--[[The data is stored as an array of bit-vectors; for each cluster, a list of which other clusters are visible
from it are stored as individual bits (1 if visible, 0 if occluded) in an array, with the nth bit position
corresponding to the nth cluster. ]]
--- @param vis VisibilityInfo
--- @param offset number
local function getClusters( vis, offset, PVS )
local c = 0
local v = offset
local pvs_buffer = vis._bytebuff
local num_clusters = vis.num_clusters
while c <= num_clusters do
if pvs_buffer[v] == 0 then
v = v + 1
c = c + 8 * pvs_buffer[v]
else
local b = 1
while b ~= 0 do
if bit.band( pvs_buffer[v], b ) ~= 0 then
PVS[c] = true
end
b = bit.band( b * 2, 0xFF )
c = c + 1
end
end
v = v + 1
end
end
--- PVS ( Potentially Visible Set )
do
--- @class PVSObject
--- @field __map BSPObject
local meta_pvs = {}
meta_pvs.__index = meta_pvs
meta_pvs.__tostring = "BSP PVS"
meta_pvs.MetaName = "BSP PVS"
NikNaks.__metatables["BSP PVS"] = meta_pvs
local DVIS_PVS = 1
--- Creates a new empty PVS-object.
--- @return PVSObject
function meta:CreatePVS()
local t = {}
t.__map = self
setmetatable( t, meta_pvs )
return t
end
--- Uses the given ( or creates a new PVS-object ) and adds the position to it.
--- @param position Vector
--- @param PVS PVSObject?
--- @return PVSObject
function meta:PVSForOrigin( position, PVS )
PVS = PVS or self:CreatePVS()
PVS.__map = self
local cluster = self:ClusterFromPoint( position )
if cluster < 0 then return PVS end -- Empty cluster position.
local vis = self:GetVisibility()
local visofs = vis.VisData[cluster].PVS
getClusters( vis, visofs, PVS )
return PVS
end
--- Returns true if the two positions are in same PVS.
--- @param position Vector
--- @param position2 Vector
--- @return boolean
function meta:PVSCheck( position, position2 )
local PVS = self:PVSForOrigin( position )
local cluster = self:ClusterFromPoint( position2 )
return PVS[cluster] or false
end
--- Adds the position to PVS
--- @param position Vector
--- @return self
function meta_pvs:AddPVS( position )
self.__map:PVSForOrigin( position, self )
return self
end
--- Removes the position from PVS
--- @param position Vector
--- @return self
function meta_pvs:RemovePVS( position )
for id in pairs( self.__map:PVSForOrigin( position ) ) do
if id ~= "__map" then self[id] = nil end
end
return self
end
--- Removes the leaf from PVS
--- @param leaf LeafObject
--- @return self PVSObject
function meta_pvs:RemoveLeaf( leaf )
self[leaf.cluster] = nil
return self
end
--- Returns true if the position is visible in the PVS
--- @param position Vector
--- @return boolean
function meta_pvs:TestPosition( position )
local cluster = self.__map:ClusterFromPoint( position )
return self[cluster] or false
end
--- Create PVS from Leaf
--- @return PVSObject
function meta_leaf:CreatePVS()
local PVS = {}
PVS.__map = self.__map
setmetatable( PVS, meta_pvs )
if self.cluster < 0 then return PVS end -- Leaf invalid. Return empty PVS.
local vis = self.__map:GetVisibility()
local visofs = vis.VisData[self.cluster].PVS
getClusters( vis, visofs, PVS )
return PVS
end
--- Returns a list of leafs within this PVS. Note: This is a bit slow.
function meta_pvs:GetLeafs()
local t = {}
local n = 1
local leafs = self.__map:GetLeafs()
for i = 1, #leafs do
local leaf = leafs[i]
local cluster = leaf.cluster
if cluster >= 0 and self[cluster] then
t[n] = leaf
n = n + 1
end
end
return t
end
--- Returns true if the PVS has the given leaf
--- @param leaf LeafObject
--- @return boolean
function meta_pvs:HasLeaf( leaf )
if leaf.cluster < 0 then return false end
return self[leaf.cluster]
end
end
-- PAS
do
---@class PASObject
local meta_pas = {}
meta_pas.__index = meta_pas
meta_pas.__tostring = "BSP PAS"
meta_pas.MetaName = "BSP PAS"
NikNaks.__metatables["BSP PAS"] = meta_pas
local DVIS_PAS = 2
--- Creates a new empty PAS-object.
--- @return PASObject
function meta:CreatePAS()
return setmetatable( {}, meta_pas )
end
--- Uses the given ( or creates a new PAS-object ) and adds the position to it.
--- @param position Vector
--- @param PAS PASObject?
--- @return PASObject?
function meta:PASForOrigin( position, PAS )
PAS = PAS or self:CreatePAS()
PAS.__map = self
local cluster = self:ClusterFromPoint( position )
local vis = self:GetVisibility()
if cluster < 0 then return end -- err
local visofs = vis.VisData[cluster].PAS
getClusters( vis, visofs, PAS )
return PAS
end
--- Returns true if the two positions are in same PAS
--- @param position Vector
--- @param position2 Vector
--- @return boolean
function meta:PASCheck( position, position2 )
local PAS = self:PASForOrigin( position )
return PAS[self:ClusterFromPoint( position2 )] or false
end
--- Adds the position to PAS
--- @param position Vector
--- @return PASObject self
function meta_pas:AddPAS( position )
self.__map:PASForOrigin( position, self )
return self
end
--- Removes the position from PAS
--- @param position Vector
--- @return PASObject self
function meta_pas:RemovePAS( position )
for id in pairs( self.__map:PASForOrigin( position ) ) do
if id ~= "__map" then self[id] = nil end
end
return self
end
--- Removes the leaf from PVS
--- @param leaf LeafObject
--- @return PASObject self
function meta_pas:RemoveLeaf( leaf )
self[leaf.cluster] = nil
return self
end
--- Returns true if the position is visible in the PAS
--- @param position Vector
--- @return boolean
function meta_pas:TestPosition( position )
local cluster = self.__map:ClusterFromPoint( position )
return self[cluster] or false
end
--- Create PAS from Leaf
--- @return PASObject
function meta_leaf:CreatePAS()
local PAS = setmetatable( {}, meta_pas )
if self.cluster < 0 then return PAS end -- Leaf invalid. Return empty PVS.
local vis = self.__map:GetVisibility()
local visofs = vis[ self.cluster ][ DVIS_PAS ]
getClusters( vis, visofs, PAS )
return PAS
end
--- Returns true if the PAS has the given leaf
--- @param leaf LeafObject
--- @return boolean
function meta_pas:HasLeaf( leaf )
if leaf.cluster < 0 then return false end
return self[leaf.cluster]
end
end

View File

@@ -0,0 +1,463 @@
--[[
| 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.
local band = bit.band
local meta = NikNaks.__metatables["BSP"]
--- @class StaticProp
--- @field Index number
--- @field version number
--- @field Origin Vector
--- @field Angles Angle
--- @field PropType string
--- @field First_leaf number
--- @field LeafCount number
--- @field Solid number
--- @field Flags? number
--- @field Skin number
--- @field FadeMinDist number
--- @field FadeMaxDist number
--- @field LightingOrigin Vector
--- @field ForcedFadeScale? number
--- @field MinDXLevel? number
--- @field MaxDXLevel? number
--- @field lightmapResolutionX? number
--- @field lightmapResolutionY? number
--- @field MinCPULevel? number
--- @field MaxCPULevel? number
--- @field MinGPULevel? number
--- @field MaxGPULevel? number
--- @field DiffuseModulation? Color
--- @field DisableX360? boolean
--- @field FlagsEx? number
--- @field UniformScale? number
local meta_staticprop = {}
meta_staticprop.__index = meta_staticprop
meta_staticprop.__tostring = function(self) return "Static Prop" .. (self.PropType and " [" .. self.PropType .. "]" or "") end
meta_staticprop.MetaName = "StaticProp"
NikNaks.__metatables["StaticProp"] = meta_staticprop
local version = {}
-- Base version from Wiki. Most HL2 maps are version 5.
version[4] = function( f, obj, m )
obj.Origin = f:ReadVector() -- Vector (3 float) 12 bytes
obj.Angles = f:ReadAngle() -- Angle (3 float) 12 bytes
obj.PropType = m[f:ReadUShort() + 1] -- unsigned short 2 bytes
obj.First_leaf = f:ReadUShort() -- unsigned short 2 bytes
obj.LeafCount = f:ReadUShort() -- unsigned short 2 bytes
obj.Solid = f:ReadByte() -- unsigned char 1 byte
obj.Flags = f:ReadByte() -- unsigned char 1 byte
obj.Skin = f:ReadLong() -- int 4 bytes
obj.FadeMinDist = f:ReadFloat() -- float 4 bytes
obj.FadeMaxDist = f:ReadFloat() -- float 4 bytes
obj.LightingOrigin = f:ReadVector() -- Vector (3 float) 12 bytes
return 448
end
-- Fade scale added.
version[5] = function( f, obj, m)
version[4]( f, obj, m )
obj.ForcedFadeScale = f:ReadFloat() -- float 4 bytes
return 480
end
-- Minimum and maximum DX-level
version[6] = function( f, obj, m)
version[5]( f, obj, m )
obj.MinDXLevel = f:ReadUShort() -- unsigned short 2 bytes
obj.MaxDXLevel = f:ReadUShort() -- unsigned short 2 bytes
return 512
end
-- Color modulation added
version[7] = function( f, obj, m )
version[6]( f, obj, m )
obj.DiffuseModulation = f:ReadColor()
return 544
end
-- Removal of DX-Level. Possible for Linux and console support.
version[8] = function( f, obj, m )
version[5]( f,obj, m )
obj.MinCPULevel = f:ReadByte() -- unsigned char 1 byte
obj.MaxCPULevel = f:ReadByte() -- unsigned char 1 byte
obj.MinGPULevel = f:ReadByte() -- unsigned char 1 byte
obj.MaxGPULevel = f:ReadByte() -- unsigned char 1 byte
obj.DiffuseModulation = f:ReadColor()
return 544
end
-- Added Dissable-flag for X360
version[9] = function( f, obj, m )
version[8]( f, obj, m )
-- The first byte seems to be the indecator.
-- All maps have the first byte as 0x00, where the L4D2 map; 'c2m4_barns.bsp', tells us it is 0x01 is true.
obj.DisableX360 = f:ReadByte() == 1 -- The first byte is the indecator
-- The last 3 bytes seems to be random data, to fill out the 32bit network-limit
f:Skip( 24 )
return 576
end
-- This version is for TF2 and some CS:S maps.
-- Was build on version 6. Guess they where never meant to be released on consoles and only PC ( Since they use DXLevel )
version[10] = function( f, obj, m )
version[6]( f, obj, m )
obj.lightmapResolutionX = f:ReadLong()
obj.lightmapResolutionY = f:ReadLong()
return 576
end
-- ( Version 7* ) This version is for some CSGO maps. I guess it was for the console support.
version["10A"] = function( f, obj, m )
version[9]( f, obj, m )
obj.FlagsEx = f:ReadULong()
return 608
end
-- The newest CSGO maps. Might have left the console's behind with the newest map versions.
version[11] = function( f, obj, m )
local q = version[9]( f, obj, m )
obj.FlagsEx = f:ReadULong()
obj.UniformScale = f:ReadFloat()
return q + 64
end
--- @class StaticProp
--- @param f BitBuffer
--- @param ver number
--- @return StaticProp, number
local function CreateStaticProp( f, ver, m )
local obj = {}
local startTell = f:Tell()
version[ver]( f, obj, m )
obj.version = ver
local sizeUsed = f:Tell() - startTell
return setmetatable( obj, meta_staticprop ), sizeUsed
end
--- Returns a list of staticprops.
--- @return StaticProp[]
function meta:GetStaticProps()
if self._staticprops then return self._staticprops end
local gameLump = self:GetGameLump( 1936749168 ) -- 1936749168 == "sprp"
local b = gameLump.buffer
local propVersion = gameLump.version
if b:Size() < 1 or not NikNaks._Source:find( "niknak" ) then -- This map doesn't have staticprops, or doesn't support them.
self._staticprops = {}
self._staticprops_mdl = {}
return self._staticprops
end
if propVersion > 11 then
ErrorNoHalt( self._mapfile .. " has an unknown static-prop version!" )
self._staticprops = {}
self._staticprops_mdl = {}
return self._staticprops
end
-- Load the model list. This list is used by the static_props.
--- @type string[]
self._staticprops_mdl = {}
local n = b:ReadLong()
if n > 16384 then -- Check if we overread the max static props.
ErrorNoHalt( self._mapfile .. " has more than 16384 models!" )
self._staticprops = {}
return self._staticprops
end
for i = 1, n do
-- All model-paths are saved as char[128]. Any overflow are nullbytes.
local model = ""
for i2 = 1, 128 do
local c = string.char( b:ReadByte() )
if string.match( c,"[%w_%-%.%/]" ) then -- Just in case, we check for "valid" chars instead.
model = model .. c
end
end
self._staticprops_mdl[i] = model
end
-- Read the leaf-array. (Unused atm). Prob an index for the static props. However each static-prop already hold said data.
b:Skip( 16 * b:ReadLong() )
-- Read static props
local count = b:ReadLong()
if count > 16384 then -- Check if we are above the max staticprop.
ErrorNoHalt( self._mapfile .. " has more than 16384 staticprops!" )
self._staticprops = {}
return self._staticprops
end
-- We calculate the amount of static props within this space. It is more stable.
local staticStart = b:Tell()
local endPos = b:Size()
local staticSize = ( endPos - staticStart ) / count
local staticUsed
--- @type StaticProp[]
self._staticprops = {}
-- Check for the 7* version.
if staticSize == 608 and propVersion == 10 then
propVersion = "10A"
end
for i = 0, count - 1 do
-- This is to try and get as much valid data we can.
b:Seek( staticStart + staticSize * i )
local sObj, sizeused = CreateStaticProp( b, propVersion, self._staticprops_mdl, staticSize )
staticUsed = staticUsed or sizeused
sObj.Index = i
self._staticprops[i] = sObj
end
if staticUsed and staticUsed ~= staticSize then
ErrorNoHalt( "Was unable to parse " .. self._mapfile .. "'s StaticProps correctly!" )
end
return self._staticprops
end
--- Returns the static-prop object from said index.
--- @param index number
--- @return StaticProp
function meta:GetStaticProp( index )
return self:GetStaticProps()[index]
end
--- Returns a list of all static-prop models used by the map.
--- @return string[]
function meta:GetStaticPropModels()
if self._staticprops_mdl then return self._staticprops_mdl end
self:GetStaticProps() -- If no model list, then load the gamelump.
return self._staticprops_mdl
end
--- Returns a list of all static-props matching the model.
--- @param model string
--- @return StaticProp[]
function meta:FindStaticByModel( model )
local t = {}
for _, v in pairs( self:GetStaticProps() ) do
if v.PropType == model then
t[#t + 1] = v
end
end
return t
end
--- Returns a list of all static-props, within the specified box.
--- @param boxMins Vector
--- @param boxMaxs Vector
--- @return StaticProp[]
function meta:FindStaticInBox( boxMins, boxMaxs )
local t = {}
for _, v in pairs( self:GetStaticProps() ) do
local origin = v.Origin
if origin and v.Origin:WithinAABox( boxMins, boxMaxs ) then
t[#t + 1] = v
end
end
return t
end
--- Returns a list of all static-props, within the specified sphere.
--- @param origin Vector
--- @param radius number
--- @return StaticProp[]
function meta:FindStaticInSphere( origin, radius )
radius = radius ^ 2
local t = {}
for _, v in pairs( self:GetStaticProps() ) do
local spOrigin = v.Origin
if spOrigin and spOrigin:DistToSqr( origin ) <= radius then
t[#t + 1] = v
end
end
return t
end
--- Returns the StaticProp index
function meta_staticprop:GetIndex()
return self.Index
end
--- Returns the origin
function meta_staticprop:GetPos()
return self.Origin
end
--- Returns the angles
function meta_staticprop:GetAngles()
return self.Angles
end
--- Returns the model path
function meta_staticprop:GetModel()
return self.PropType
end
--- Returns the skin index
function meta_staticprop:GetSkin()
return self.Skin or 0
end
--- @return Color
function meta_staticprop:GetColor()
return self.DiffuseModulation or color_white
end
function meta_staticprop:GetScale()
return self.UniformScale or 1
end
meta_staticprop.GetModelScale = meta_staticprop.GetScale
--- Returns the solid enum. See: https://wiki.facepunch.com/gmod/Enums/SOLID
function meta_staticprop:GetSolid()
return self.Solid
end
--- Returns the lighting origin
function meta_staticprop:GetLightingOrigin()
return self.LightingOrigin
end
--- Returns the flags
function meta_staticprop:GetFlags()
return self.Flags
end
--- Returns true if the staticprop has a flag.
--- @param flag number
--- @return boolean
function meta_staticprop:HasFlag( flag )
return band( self:GetFlags(), flag ) ~= 0
end
--- Returns true if the static prop is disabled on X360.
--- @return boolean
function meta_staticprop:GetDisableX360()
return self.DisableX360 or false
end
--- Returns the model bounds.
--- @return Vector
--- @return Vector
function meta_staticprop:GetModelBounds()
local a, b = NikNaks.ModelSize( self:GetModel() )
local s = self:GetScale()
return a * s, b * s
end
meta_staticprop.GetModelRenderBounds = meta_staticprop.GetModelBounds
meta_staticprop.GetRenderBounds = meta_staticprop.GetModelBounds
-- Fade Functions
function meta_staticprop:GetFadeMinDist()
return self.FadeMinDist
end
function meta_staticprop:GetFadeMaxDist()
return self.FadeMaxDist
end
function meta_staticprop:GetForceFadeScale()
return self.ForcedFadeScale or 1
end
-- "Other"
--[[ DXLevel
0 = Ignore
70 = DirectX 7
80 = DirectX 8
81 = DirectX 8.1
90 = DirectX 9
95 = DirectX 9+ ( 9.3 )
98 = DirectX 9Ex
]]
function meta_staticprop:GetDXLevel()
return self.MinDXLevel or 0, self.MaxDXLevel or 0
end
if CLIENT then
-- Checks to see if the client has the directX level required to render the static prop.
function meta_staticprop:HasDXLevel()
local num = render.GetDXLevel()
if self.MinDXLevel ~= 0 and num < self.MinDXLevel then return false end
if self.MaxDXLevel ~= 0 and num > self.MaxDXLevel then return false end
return true
end
end
--[[ There must be a list of CPU's and what level they are.
CPU Level
0 = Ignore
1 = "Low"
2 = "Medium"
3 = "High"
]]
function meta_staticprop:GetCPULevel()
return self.MinCPULevel or 0, self.MaxCPULevel or 0
end
--[[ There must be a list of GPU's and what level they are.
GPU Level
0 = Ignore
1 = "Low"
2 = "Medium"
3 = "High"
]]
function meta_staticprop:GetGPULevel()
return self.MinGPULevel or 0, self.MaxGPULevel or 0
end
-- Allows to set the lightmap resolution for said static-prop.
-- Checkout https://tf2maps.net/threads/guide-lightmap-optimization.33113/ for more info
function meta_staticprop:GetLightMapResolution()
return self.lightmapResolutionX, self.lightmapResolutionY
end
--- Returns the "Further" BitFlags. Seems to be only used for the "STATIC_PROP_FLAGS_EX_DISABLE_CSM" flag.
--- @return number
function meta_staticprop:GetFlagExs()
return self.FlagsEx or 0
end
--- Returns true if the staticprop has an exflag.
--- @param flag number
--- @return boolean
function meta_staticprop:HasFlagEx( flag )
return band( self:GetFlagExs(), flag ) ~= 0
end
--- Returns the version of the static props.
--- Note: version 7* will be returned as a string: "10A"
function meta_staticprop:GetVersion()
return self.version
end

View File

@@ -0,0 +1,203 @@
--[[
| 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.
local NikNaks = NikNaks
local COLOR = FindMetaTable("Color")
local clamp = math.Clamp
local Color, round = Color, math.Round
local min, max, abs = math.min, math.max, math.abs
local string_format, string_sub = string.format, string.sub
-- Color enums
NikNaks.SERVER_COLOR= Color(156, 241, 255, 200)
NikNaks.CLIENT_COLOR= Color(255, 241, 122, 200)
NikNaks.MENU_COLOR = Color(100, 220, 100, 200)
NikNaks.REALM_COLOR = SERVER and NikNaks.SERVER_COLOR or CLIENT and NikNaks.CLIENT_COLOR or MENU_DLL and NikNaks.MENU_COLOR
NikNaks.color_error_server = Color(136, 221, 255)
NikNaks.color_error_client = Color(255, 221, 102)
NikNaks.color_error_menu = Color(120, 220, 100)
---Returns the luminance amount. How "bright" a color is between 0 and 255.
---@param color Color
---@return number
function NikNaks.ColorToLuminance(color)
return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b
end
---Returns the luminance amount. How "bright" a color is between 0 and 255.
---@return number
function COLOR:ToLuminance()
return 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
end
-- Hex
---Converts a color into a hex-string.
---@param color Color
---@return string
function NikNaks.ColorToHex(color)
return "#" .. string_format("%X", color.r) .. string_format("%X", color.g) .. string_format("%X", color.b)
end
---Converts a color into a hex-string.
---@return string
function COLOR:ToHex()
return ColorToHex(self)
end
---Converts a hex-stirng into a color.
---@param str string
---@return Color
function NikNaks.HexToColor(str)
str = string.gsub(str,"#","")
local r = round( tonumber( string_sub(str,1,2), 16) )
local g = round( tonumber( string_sub(str,3,4), 16) )
local b = round( tonumber( string_sub(str,5,6), 16) )
return Color(r, g, b)
end
-- CMYK
---Converts a color into CMYK variables.
---@return number c
---@return number m
---@return number y
---@return number j
function COLOR:ToCMYK()
local r, g, b = self.r / 255, self.g / 255, self.b / 255
local k = 1 - max(r, g, b)
local c = (1 - r - k) / ( 1 - k )
local m = (1-g-k) / ( 1 - k )
local y = (1-b-k) / ( 1 - k )
return c, m, y, k
end
---Converts a color into CMYK variables.
---@return number c
---@return number m
---@return number y
---@return number j
function NikNaks.ColorToCMYK( color )
return color:ToCMYK()
end
---Converts CMYK variables into a color.
---@param c any
---@param m any
---@param y any
---@param k any
---@return Color
function NikNaks.CMYKToColor( c, m, y, k )
local r = math.Round( 255 * ( 1 - c ) * ( 1 - k ) )
local g = math.Round( 255 * ( 1 - m ) * ( 1 - k ) )
local b = math.Round( 255 * ( 1 - y ) * ( 1 - k ) )
return Color( r, g, b )
end
-- Color manipulation
---Brightens the color by [0-255]-amount.
---@param amount number
---@return Color
function COLOR:Brighten(amount)
local h,s,l = ColorToHSL(self)
return HSLToColor(h,s,l + amount)
end
---Darkens the color by [0-255]-amount.
---@param amount number
---@return Color
function COLOR:Darken(amount)
return self.lighten(-amount)
end
---Inverts the color.
---@return Color
function COLOR:Invert()
return Color(255 - self.r,255 - self.g,255 - self.b)
end
---Turns the color into a gray-scale.
---@return Color
function COLOR:ToGrayscale()
local H,S,L = self:ToHSL()
return HSLToColor(H,0,L)
end
---Cartoonify the color.
---@param color Color
---@return Color
function COLOR:ToCartoon(color)
local R,G,B = color.r / 255,color.g / 255,color.b / 255
local max_gb = max(G,B)
local max_rb = max(R,B)
local max_rg = max(R,G)
local red_matter = 1 - max(R - max_gb,0)
local green_matter = 1 - max(G - max_rb,0)
local blue_matter = 1 - max(B - max_rg,0)
return Color(R * green_matter * blue_matter * 255,G * red_matter * blue_matter * 255,B * red_matter * green_matter * 255)
end
-- Color functions
---Returns true if the color is bright. Useful to check if the text infront should be dark.
---@return boolean
function COLOR:IsBright()
return ColorToLuminance(self) >= 127.5
end
---Returns true if the color is bright. Useful to check if the text infront should be bright.
---@return boolean
function COLOR:IsDark()
return ColorToLuminance(self) < 127.5
end
-- ColorRGBExp32
local gamma = 2.2
local overbrightFactor = 0.5
-- convert texture to linear 0..1 value
local function TexLightToLinear( col, exponent )
return col * ( ( 2 ^ exponent ) / 255 )
end
-- linear (0..4) to screen corrected vertex space (0..1?)
local function LinearToVertexLight( col )
return overbrightFactor * ( col ^ ( 1 / gamma ) )
end
-- https://github.com/ValveSoftware/source-sdk-2013/blob/master/sp/src/utils/vrad/lightmap.cpp#L3551
function NikNaks.ColorRGBExp32ToColor( struct )
local exponent = struct.exponent
local linearColor = {
TexLightToLinear( struct.r, exponent ),
TexLightToLinear( struct.g, exponent ),
TexLightToLinear( struct.b, exponent )
}
local vertexColor = {
math.min( LinearToVertexLight( linearColor[1] ), 1 ),
math.min( LinearToVertexLight( linearColor[2] ), 1 ),
math.min( LinearToVertexLight( linearColor[3] ), 1 )
}
return Color(
math.Round( vertexColor[1] * 255 ),
math.Round( vertexColor[2] * 255 ),
math.Round( vertexColor[3] * 255 ),
255
)
end

View File

@@ -0,0 +1,401 @@
--[[
| 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
NikNaks.DateTime = {}
local localvars, os_time, os_date, rawget, tonumber, getmetatable = {}, os.time, os.date, rawget, tonumber, getmetatable
-- TimeZone / Date variables
do
local UTC_DAY = os_date( "%d", 0 ) - os_date( "!%d", 0 )
local UTC_Timezone = tonumber( os_date( "%H", 0 ) ) - tonumber( os_date( "!%H", 0 ) )
if UTC_DAY == 30 then
UTC_Timezone = UTC_Timezone - 24
end
local UTC_Timezone_dst = tonumber( os_date( "%z" ) ) / 100
local DaylightsSaving = UTC_Timezone_dst - UTC_Timezone
NikNaks.DateTime.dst = DaylightsSaving
NikNaks.DateTime.timezone = UTC_Timezone
NikNaks.DateTime.timezone_dst = UTC_Timezone_dst
end
local function is_leap_year( year )
return year % 4 == 0 and ( year % 100 ~= 0 or year % 400 == 0 )
end
function NikNaks.DateTime.IsLeapYear( year )
return is_leap_year( year or NikNaks.DateTime.year )
end
do
local months = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
function NikNaks.DateTime.DaysInMonth( month, year )
if month == 2 and is_leap_year( year or NikNaks.DateTime.year ) then
return 29
end
return months[month]
end
function NikNaks.DateTime.Calender( year )
year = year or NikNaks.DateTime.year
local c = {}
c.year = year
c.month = {}
for i = 1, 12 do
if i == 2 and is_leap_year( year ) then
c.month[i] = 29
else
c.month[i] = months[i]
end
end
return c
end
end
-- Date variables
local function updatedate()
local date = string.Explode( ":", os_date( "%H:%M:%S:%d:%m:%Y" ) )
NikNaks.DateTime.day = tonumber( date[4] )
NikNaks.DateTime.month = tonumber( date[5] )
NikNaks.DateTime.year = tonumber( date[6] )
-- Calculates next cycle
local t_seconds = tonumber( date[1] ) * 3600 + tonumber( date[2] ) * 60 + tonumber( date[3] )
local nextUpdate = 86400 - t_seconds
timer.Create( "NikNaks_DateUpdate", math.max( nextUpdate, 1 ), 1, updatedate )
end
updatedate()
-- Branch metatable
setmetatable( NikNaks.DateTime, {
__index = function( _, v )
local l = rawget( localvars, v )
return rawget( NikNaks.DateTime, v ) or l and l()
end,
__call = function( _, var )
return NikNaks.DateTime.Get( var )
end
} )
local string_to_var
do
-- Tries to parse hour, minute and seconds
local function findTime( str )
local h, m, s, ampm = string.match( str:upper(), "([01]?%d):(%d%d?):?(%d*)%s*([AP][M])" )
if not h then
h, m, s = string.match( str, "(%d%d?):(%d%d?):?(%d*)" )
end
if not h then return nil end
h = tonumber( h )
m = tonumber( m )
s = tonumber( s )
if ampm then
if ampm == "AM" then
if h == 12 then h = 0 end
else
if h ~= 12 then h = h + 12 end
end
end
return h, m, s
end
-- Tries to parse year, month, day
local findDate
do
local date_tab = {
"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
}
local date_pattern = "[JFMASOND][AEPUCO][NBRYLGPTVC]"
local function findMonthNameAndDate( str )
if not string.match( str, date_pattern ) then return nil end
for m_id, date in ipairs( date_tab ) do
if string.match( str, date ) then
local d = string.match( str, date .. "%a*%s?(%d%d?)" ) or string.match( str, "(%d%d?)%s?" .. date )
return m_id, d and tonumber( d ) or 1
end
end
end
function findDate( str )
-- The year number tent to mess with the rest, if found replace it if found.
local fy = true
local y = string.match( str, "(%d%d%d%d)" )
local m, d
if y then
str = string.gsub( str, "%d%d%d%d", "", 1 )
y = tonumber( y )
else
-- Check of YY/MM/DD
y, m, d = string.match( str, "(%d+)[/%-](%d%d?)[/%-](%d%d?)" )
if not y then -- Year must be today
y = NikNaks.DateTime.year
fy = false
else
return tonumber( y ), tonumber( m ), tonumber( d )
end
end
-- Find MM/DD
m, d = string.match( str, "(%d%d?)[/%-](%d%d?)" )
if m and d then
return y, tonumber( m ), tonumber( d )
end
-- No date found. Try string-scan for month names
m, d = findMonthNameAndDate( str:upper() ) -- Try parse letters
if m then
return y, m, d
end
-- If only a year is given, then return the first day in that year.
if fy then
return y, 1, 1
end
end
end
-- Tries to parse timezone. Since os.time use the locate time, this will be negative.
local function findOffset( str )
if str:sub( -1 ) == "Z" then return -NikNaks.DateTime.timezone_dst end
local sign, h, m = str:match( "([%-%+])(%d%d?):?(%d?%d?)$" )
if sign then
return ( tonumber( sign .. h ) + tonumber( sign .. m ) / 60 ) - NikNaks.DateTime.timezone_dst
else
-- Use local
return 0
end
end
function string_to_var( str )
str = string.Trim( str )
if #str ~= 4 then
local n = string.match( str, "%d+" )
if #n == #str then return tonumber( n ) end
end
--[[
Sun, 03 Jan 2010 00:00:00 GMT
September 26, 2006 12:12 AM
2012-10-06T04:13:00+00:00
2012/10/6
2008-05-01T07:34:42-5:00
2008-05-01 7:34:42Z
Thu, 01 May 2008 07:34:42 GMT
]]
local h, m, s = findTime( str )
-- Get Time & Date
local year, month, day = findDate( str )
-- Find offset
local offsetH = findOffset( str ) or 0
-- Convert to unix
return os_time( {
day = day or 1,
hour = h or 0,
min = m or 0,
month = month or 1,
sec = tonumber( s ) or 0,
year = year
} ) + offsetH * 3600
end
end
--- @class DateTime
local datetime_obj = {}
datetime_obj.__index = datetime_obj
NikNaks.__metatables["DateTime"] = datetime_obj
function NikNaks.DateTime.Get( var, t_zone )
if not var then
var = os_time()
else
local _type = type( var )
if _type == "string" then
var = string_to_var( var )
elseif _type == "table" then
if var.time then
var = var.time + os_time()
elseif var.unix then
var = var.unix
else -- Unknown
return nil
end
end
end
-- Unable to create
if not var then return nil end
-- Create object and return
--- @class DateTime
local t = {}
t.unix = var
t.timezone = t_zone
return setmetatable( t, datetime_obj )
end
function datetime_obj:GetUnix()
return self.unix
end
function datetime_obj:TimeUntil( var )
local unix
local _type = type( var )
if _type == "string" then
unix = string_to_var( var )
elseif _type == "table" then
if var.time then
return var.time -- Will always be relative
elseif var.unix then
unix = var.unix
end
end
return NikNaks.TimeDelta( unix - self.unix, tonumber( os.date( "%Y", self.unix ) ) )
end
-- Local variable functions: DateTime.<X>
function localvars.now()
return NikNaks.DateTime.Get( os_time() )
end
localvars.today = localvars.now
function localvars.yesterday()
return NikNaks.DateTime.Get( os_time() - NikNaks.TimeDelta.Day )
end
function localvars.tomorrow()
return NikNaks.DateTime.Get( os_time() + NikNaks.TimeDelta.Day )
end
--- Returns the time using os.date
--- @param format string
--- @return string
function datetime_obj:ToDate( format )
return os_date( format, self.unix )
end
datetime_obj.__tostring = function( self )
return os_date( nil, self.unix )
end
-- Operations
function datetime_obj.__sub( a, b )
if not getmetatable( a ) then -- A is most likely a number. Number - Obj = TimeDelta
return NikNaks.TimeDelta( a - b.unix )
elseif not getmetatable( b ) then -- B is most likely a number. Obj - Number = New Obj
return NikNaks.DateTime.Get( a.unix - b )
else -- Both are objects
if a.unix and b.unix then
return NikNaks.TimeDelta( a.unix - b.unix )
elseif a.unix then
return NikNaks.DateTime.Get( a.unix - ( b.time or b ) )
elseif b.unix then
return NikNaks.DateTime.Get( b.unix - ( a.time or a ) )
end
end
end
function datetime_obj.__add( a, b )
if not getmetatable( a ) then -- A is most likely a number. Number + Obj = New Obj
return NikNaks.DateTime.Get( b.unix + a )
elseif not getmetatable( b ) then -- B is most likely a number. Obj - Number = New Obj
return NikNaks.DateTime.Get( a.unix + b )
else -- Both are objects
if a.unix and b.unix then -- Get the higest unix-time and add the delta between the two
return NikNaks.DateTime.Get( math.max( a.unix, b.unix ) + abs( a.unix - b.unix ) )
elseif a.unix then
return NikNaks.DateTime.Get( a.unix + ( b.time or b ) )
elseif b.unix then
return NikNaks.DateTime.Get( b.unix + ( a.time or a ) )
end
end
end
function datetime_obj.__concat( a, b )
return tostring( a ) .. tostring( b )
end
-- Not supported in Gmod!
datetime_obj.__shl = datetime_obj.__sub
datetime_obj.__shr = datetime_obj.__add
-- Sadly Lua doesn't support mixed types for compare-operations
function datetime_obj.__eq( a, b )
return a.unix == b.unix
end
function datetime_obj.__lt( a, b )
return a.unix < b.unix
end
function datetime_obj.__le( a, b )
return a.unix <= b.unix
end
-- TimeDelta functions
for key, var in pairs( NikNaks.TimeDelta ) do
datetime_obj["Add" .. key .. "s"] = function( self, num )
self.unix = self.unix + num * var
return self
end
datetime_obj["Remove" .. key .. "s"] = function( self, num )
self.unix = self.unix - num * var
return self
end
datetime_obj["Sub" .. key .. "s"] = datetime_obj["Remove" .. key .. "s"]
end
-- DateTime string debug test
if true then return end
local t = { "Sun, 01 Sep 2022 00:12:00",
"September 01, 2022 12:12 AM",
"2022-09-01T00:12:00+02:00",
"2022/09/1",
"2022-09-01T07:12:00-5:00",
"2022-09-01 02:12:00Z",
"Thu, 01 Sep 2022 00:12:00" }
function ParseTest()
for _, str in ipairs( t ) do
print( str .. string.rep( " ", 30 - #str ), "=>", NikNaks.DateTime.Get( str ) )
end
end
function SpeedTest()
local n = 20000 * #t
local s = SysTime()
for _, str in ipairs( t ) do
for _ = 1, 20000 do
NikNaks.DateTime.Get( str )
end
end
print( string.format( n .. " took: %fs", SysTime() - s ) )
end

View File

@@ -0,0 +1,68 @@
--[[
| 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.
-- Globals
NikNaks.vector_zero = Vector( 0, 0, 0 )
NikNaks.vector_down = Vector( 0, 0, -1 )
NikNaks.angle_up = vector_up:Angle()
NikNaks.angle_down = NikNaks.vector_down:Angle()
-- CAP
NikNaks.CAP_MOVE_GROUND = 0x01 -- walk/run
NikNaks.CAP_MOVE_JUMP = 0x02 -- jump/leap
NikNaks.CAP_MOVE_FLY = 0x04 -- can fly, move all around
NikNaks.CAP_MOVE_CLIMB = 0x08 -- climb ladders
--CAP_MOVE_SWIM / bits_BUILD_GIVEWAY? = 0x10 -- navigate in water // Removed by Valve: UNDONE - not yet implemented
--CAP_MOVE_CRAWL = 0x20 -- crawl // Removed by Valve: UNDONE - not yet implemented
-- Nodes
NikNaks.NODE_TYPE_INVALID =-1 -- Any nodes not matching these
NikNaks.NODE_TYPE_ANY = 0
NikNaks.NODE_TYPE_DELETED = 1 -- Internal in hammer?
NikNaks.NODE_TYPE_GROUND = 2
NikNaks.NODE_TYPE_AIR = 3
NikNaks.NODE_TYPE_CLIMB = 4
--NODE_TYPE_WATER = 5 -- Unused? I have no idea, since CAP_MOVE_SWIM seems unused and the fish use air nodes.
-- Hulls
NikNaks.HULL_HUMAN = 0 -- 30w, 73t // Combine, Stalker, Zombie...
NikNaks.HULL_SMALL_CENTERED = 1 -- 40w, 40t // Scanner
NikNaks.HULL_WIDE_HUMAN = 2 -- ? // Vortigaunt
NikNaks.HULL_TINY = 3 -- 24w, 24t // Headcrab
NikNaks.HULL_WIDE_SHORT = 4 -- ? // Bullsquid
NikNaks.HULL_MEDIUM = 5 -- 36w, 65t // Cremator
NikNaks.HULL_TINY_CENTERED = 6 -- 16w, 8t // Manhack
NikNaks.HULL_LARGE = 7 -- 80w, 100t // Antlion Guard
NikNaks.HULL_LARGE_CENTERED = 8 -- ? // Mortar Synth / Strider
NikNaks.HULL_MEDIUM_TALL = 9 -- 36w, 100t // Hunter
NikNaks.NUM_HULLS = 10
-- HULL_NONE = 11 Used internal I think.
-- Errors
NikNaks.BSP_ERROR_FILECANTOPEN = 0
NikNaks.BSP_ERROR_NOT_BSP = 1
NikNaks.BSP_ERROR_TOO_NEW = 2
NikNaks.BSP_ERROR_FILENOTFOUND = 3
NikNaks.AIN_ERROR_VERSIONNUM = 4
NikNaks.AIN_ERROR_ZONEPATCH = 5 -- This error is thrown when the AIN-parser repairs the data. It will still return the data successfully.
-- naksbot
NikNaks.PATHTYPE_NONE=-1 -- In case there are no path-options on the map
NikNaks.PATHTYPE_AIN = 0
NikNaks.PATHTYPE_NAV = 1
NikNaks.PATHTYPE_NIKNAV = 2
-- How the NPC should move
NikNaks.PATHMOVETYPE_GROUND = 0
NikNaks.PATHMOVETYPE_FLY = 1

View File

@@ -0,0 +1,58 @@
--[[
| 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.
local FILE = FindMetaTable( "File" )
-- File functions
--- Returns true if the file is valid and can be written to.
--- @return boolean
function FILE:IsValid()
return tostring( self ) ~= "[NULL File]"
end
--- Writes a vector to the file.
--- @param vector Vector
function FILE:WriteVector( vector )
self:WriteFloat( vector.x )
self:WriteFloat( vector.y )
self:WriteFloat( vector.z )
end
--- Reads a vector from the file.
--- @return Vector
function FILE:ReadVector()
return Vector( self:ReadFloat(), self:ReadFloat(), self:ReadFloat() )
end
NikNaks.file = {}
--- Same as file.Write, but will automatically create folders and return true if successful.
--- @param fileName string
--- @param contents string
--- @return boolean
function NikNaks.file.WriteEx( fileName, contents )
local a = string.Explode( "/", fileName )
assert( #a <= 10, "Unable to create an unreasonable array of folders!" )
if #a > 1 then
file.CreateDir( string.GetPathFromFilename( fileName ) )
end
local f = file.Open( fileName, "wb", "DATA" )
if not f then return false end
f:Write( contents )
f:Close()
return true
end

View File

@@ -0,0 +1,69 @@
--[[
| 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.
local NikNaks = NikNaks
do
local file_Open, file_Exists = file.Open, file.Exists
local cache = {}
--- Returns the model's hull size.
--- @param name string
--- @return Vector MinVec
--- @return Vector MaxVec
function NikNaks.ModelSize( name )
if cache[name] then
return Vector( cache[name][1] ), Vector( cache[name][2] )
end
if not file_Exists( name, "GAME" ) then
cache[name] = { NikNaks.vector_zero, NikNaks.vector_zero }
return Vector( cache[name][1] ), Vector( cache[name][2] )
end
local f = file_Open( name, "r", "GAME" )
f:Seek( 104 )
local hullMin = Vector( f:ReadFloat(), f:ReadFloat(), f:ReadFloat() )
local hullMax = Vector( f:ReadFloat(), f:ReadFloat(), f:ReadFloat() )
f:Close()
cache[name] = { hullMin, hullMax }
return Vector( hullMin ), Vector( hullMax )
end
end
do
local util_GetModelMeshes, Material = util.GetModelMeshes, Material
--- Returns the materials used for this model. This can be expensive, so cache the result.
--- @param name any
--- @param lod? number
--- @param bodygroupMask? number
--- @return table
function NikNaks.ModelMaterials( name, lod, bodygroupMask )
local data = util_GetModelMeshes( name, lod or 0, bodygroupMask or 0 )
if not data then return {} end
local t = {}
for i = 1, #data do
local mat = data[i]["material"]
if mat then
table.insert( t, Material( mat ) )
end
end
return t
end
end

View File

@@ -0,0 +1,438 @@
--[[
| 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.
local NikNaks = NikNaks
local CurTime, setmetatable, IsValid = CurTime, setmetatable, IsValid
local min, max, cos = math.min, math.max, math.cos
--- @class LPathFollower
--- @field _segments LPathFollowerSegment[]
local meta = {}
meta.__index = meta
meta.__tostring = function( self )
return "LPathFollower Age: " .. self:GetAge()
end
NikNaks.__metatables["LPathFollower"] = meta
--[[
t._segments = {}
t._entity = nil -- Overrides the last position in the segments
t._age = CurTime()
]]
--- Creates an empty path-follower
--- @param start_pos Vector
--- @return LPathFollower
function meta.CreatePathFollower( start_pos )
--- @class LPathFollower
local t = {}
t._segments = {}
t._start = start_pos
t._length = 0
t._age = CurTime()
t._valid = true
t._cursor = 1
t._cursor_dis = 0
t._cursor_lef = 0
return setmetatable( t, meta )
end
--- Adds a segment.
--- @param from Vector
--- @param to Vector
--- @param curvature number
--- @param move_type number
--- @return LPathFollowerSegment
function meta:AddSegment( from, to, curvature, move_type )
--- @class LPathFollowerSegment
local seg = {}
seg.curvature = curvature or 0
seg.move_type = move_type or 1
seg.distanceFromStart = from:Distance( to )
local n = ( to - from ):GetNormalized()
seg.forward = n
seg.yaw = ( -n ):Angle().y
if n.z > 0.7 or n.z < -0.7 then
self.how = 9
else
if n.y < -0.5 then -- North
seg.how = 0
elseif n.y > 0.5 then -- South
seg.how = 2
elseif n.x > 0.5 then -- East
seg.how = 1
else -- West
seg.how = 3
end
end
seg.length = from:Distance( to )
seg.s_lengh = self._length
seg.pos = to
--seg.ladder
--seg.node
--seg.area
--seg.nna
self._length = self._length + seg.length
self._segments[#self._segments + 1] = seg
return seg
end
-- Default easy functions
do
--- Returns the length of the path.
function meta:GetLength()
return self._length or 0
end
--- Returns the first segment.
function meta:FirstSegment()
return self._segments[1]
end
--- Returns the last segment.
function meta:LastSegment()
return self._segments[#self._segments]
end
--- Returns all segments.
function meta:GetAllSegments()
return self._segments
end
--- Returns the age of the path.
function meta:GetAge()
return CurTime() - self._age
end
--- Resets the age of the path.
function meta:ResetAge()
self._age = CurTime()
end
--- Returns true if the path is valid.
function meta:IsValid()
return self._valid or false
end
--- Invalidates the path.
function meta:Invalidate()
self._valid = false
end
--- Returns the starting position of the path.
function meta:GetStart()
return self._start
end
--- Returns the ending position of the path (Note, will update if the target is an entity).
function meta:GetEnd()
if IsValid( self.target_ent ) then
return self.target_ent:GetPos()
end
return self._segments[#self._segments].pos
end
end
-- Cursor
local findClosestSeg
do
--- Returns the cursor position.
--- @return number
--- @return number
local function findCursor( self, distance )
local q = self._segments
distance = min( self._length, distance )
for i = 1, #q do
if distance <= q[i].s_lengh + q[i].length then
return i, distance - ( q[i].s_lengh + q[i].length )
end
end
return 1, distance
end
--- Returns the closest segment and its index.
--- @param position Vector
--- @return LPathFollowerSegment
--- @return number
findClosestSeg = function( self, position )
local c, d, q
for i, seg in pairs( self._segments ) do
local dis = seg.pos:DistToSqr( position )
if not c or c > dis then
c = dis
d = seg
q = i
end
end
return d, q
end
--- Returns the closest position along the path to said position.
--- @param position Vector
--- @return Vector
local function findClosestBetween( A, dir, position, maxLength )
local v = position - A
local d = v:Dot( dir )
return A + dir * max( 0, min( d, maxLength ) )
end
--- Returns the position on the path by given distance
--- @param distance number
--- @return Vector
function meta:GetPositionOnPath( distance )
local seg_id, lef = findCursor( self, distance )
local t = self._segments
local seg = t[seg_id]
return seg.pos + seg.forward * lef
end
--- Returns the closest position along the path to said position.
--- @param position Vector
--- @return Vector
function meta:GetClosestPosition( position )
-- Locate the closest points
local seg, seg_id = findClosestSeg( self, position )
if not seg then return self:GetStart() end -- Fallback to the start pos
local max_seg = #self._segments
if seg_id <= 1 then
local n_seg = self._segments[seg_id + 1]
local v1 = findClosestBetween( self:GetStart(), seg.forward, position, seg.length )
local v2 = findClosestBetween( seg.pos, n_seg.forward, position, n_seg.length )
if v1:DistToSqr( position ) > v2:DistToSqr( position ) then
return v2
else
return v1
end
elseif seg_id >= max_seg then
local s_seg = self._segments[max_seg - 1]
return findClosestBetween( s_seg.pos, seg.forward, position, seg.length )
else
local p_seg = self._segments[seg_id - 1]
local n_seg = self._segments[seg_id + 1]
local v1 = findClosestBetween( p_seg.pos, seg.forward, position, seg.length )
local v2 = findClosestBetween( seg.pos, n_seg.forward, position, n_seg.length )
if v1:DistToSqr( position ) > v2:DistToSqr( position ) then
return v2
else
return v1
end
end
end
--- Moves the cursor to the start of the path.
function meta:MoveCursorToStart()
self._cursor_dis = 0
self._cursor_lef = 0
self._cursor = 1
end
--- Moves the cursor to the end of the path.
function meta:MoveCursorToEnd()
self._cursor_dis = self:GetLength()
self._cursor = #self._segments
self._cursor_lef = self._segments[self._cursor].length
end
--- Returns the cursor progress along the path
--- @return number
function meta:GetCursorPosition()
return self._cursor_dis
end
--- Moves the cursor to said distance.
--- @param distance number
function meta:MoveCursorTo( distance )
self._cursor_dis = distance
local seg_id, lef = findCursor( self, distance )
self._cursor = seg_id
self._cursor_lef = lef
end
--- Moves the cursor said distance
--- @param distance number
function meta:MoveCursor( distance )
self._cursor_dis = self._cursor_dis + distance
local nd = self._cursor_lef + distance
local seg = self._segments[self._cursor]
if nd < 0 or nd > seg.length then -- New segment
local seg_id, lef = findCursor( self, self._cursor_dis )
self._cursor = seg_id
self._cursor_lef = lef
else
self._cursor_lef = nd
end
end
--- Returns the closest sequence.
--- @param position Vector
--- @return LPathFollowerSegment
--- @return number
function meta:FindClosestSeg( position )
return findClosestSeg( self, position )
end
--- Returns the distance from the path.
--- @param position Vector
--- @return number
function meta:FindDistanceFromPath( position )
return self:GetClosestPosition( position ):Distance( position )
end
end
-- NPC stuff
do
--[[
By default in Gmod, you're reuired to create a pathfind object, and then compute it.
However I've found many situations where I could reuse a path. I.e a group of NPC's.
So to stick to the closest "Gmod way", path:Update can be called on multiple entities and got a goal argument.
C function here: https://github.com/Joshua-Ashton/Source-PlusPlus/blob/4056819cea889d73626a1cbc09518b2f8ba5dda4/src/game/shared/cstrike/bot/nav_path.cpp#L472
]]
function meta:Update( ent, toleranceSqrt )
-- Make sure it is a valid entity and it has loco
if not IsValid( ent ) or not ent.loco then return true end
local entPos = ent:GetPos()
local seq, id = nil, ent._cursor
-- Located the closest segment, and use that
if not id then
seq, id = findClosestSeg( self, entPos )
ent._cursor = id
else
seq = self._segments[id]
end
if not seq then return true end -- No segment, we must have reached the end
local goal = seq.pos
if goal:DistToSqr( entPos ) < toleranceSqrt then
if id >= #self._segments then return true end -- Reached the end
id = id + 1
seq = self._segments[id]
goal = seq.pos
ent._cursor = ent._cursor + 1
end
ent.loco:Approach( goal, 1 )
ent.loco:FaceTowards( goal )
ent:SetAngles( Angle( 0, ( ent:GetPos() - goal ):Angle().y, 0 ) )
end
end
-- NET
do
function NikNaks.net.WritePath( path )
local n = #path._segments
net.WriteUInt( n, 16 )
net.WriteFloat( path._age )
net.WriteVector( path._start )
for i = 1, n do
local seg = path._segments[i]
net.WriteVector( seg.pos - seg.length * seg.forward )
net.WriteVector( seg.pos )
net.WriteFloat( seg.curvature )
net.WriteUInt( seg.move_type, 8 )
end
end
function NikNaks.net.ReadPath()
local n = net.ReadUInt( 16 )
local age = net.ReadFloat()
local path = meta.CreatePathFollower( net.ReadVector() )
path._age = age
for _ = 1, n do
path:AddSegment( net.ReadVector(), net.ReadVector(), net.ReadFloat(), net.ReadUInt( 8 ) )
end
return path
end
end
-- Debug
do
local mat_goal = Material("editor/assault_rally")
local mat_start = Material("effects/powerup_agility_hud")
local tMat = Material("effects/bluelaser1")
local m = Material("hud/arrow_big")
local point = Material("hud/freezecam_callout_arrow")
local cir = Material("hud/cart_point_neutral_opaque")
local mat_arrow = Material("vgui/glyph_expand")
local mat_solider = Material("hud/bomb_carried")
local jump_man = Material("hud/death_wheel_1")
local col_walk = color_white
local col_climb = Color(155,55,155)
local col_fly = Color(55,55,255)
local col_jump = Color(125, 55, 255)
local mov = bit.bor(NikNaks.CAP_MOVE_GROUND, NikNaks.CAP_MOVE_CLIMB, NikNaks.CAP_MOVE_JUMP)
local point_b = Material("hud/cart_point_blue")
local point_c = Material("hud/expanding_vert_middle_blue_bg")
function meta:DebugRender()
local l
for i, seg in ipairs(self:GetAllSegments()) do
render.SetMaterial(cir)
render.DrawSprite( seg.pos, 16, 16 )
if l then
local col = color_white
if seg.move_type == NikNaks.CAP_MOVE_CLIMB then
col = col_climb
elseif seg.move_type == NikNaks.CAP_MOVE_FLY then
col = col_fly
elseif seg.move_type == NikNaks.CAP_MOVE_JUMP then
col = col_jump
end
local d = seg.pos:Distance(l)
local n = (SysTime() * 2) % 1
m:SetVector("$color",Vector(col.r,col.g,col.b) / 255)
render.SetMaterial(m)
render.DrawBeam( seg.pos, l, 30, n, d / 40 + n, col )
render.SetMaterial(point)
render.DrawBeam( l + seg.forward * 10,l + seg.forward * 30, 20, 0, 1)
end
l = seg.pos
end
local num = max(1, self:GetLength() / 800)
for i = 1, num do
local dis = (CurTime() * 100 + i * 800) % self:GetLength()
local pos = self:GetPositionOnPath( dis )
local q = 1 + cos(SysTime() * 10 + i) * 0.1
render.SetMaterial(mat_arrow)
render.DrawBeam( pos + Vector(0,0,16), pos, 16, q - 1, q, color_white )
render.SetMaterial(mat_solider)
render.DrawSprite( pos + Vector(0,0,20), 16, 16 )
end
end
end

View File

@@ -0,0 +1,290 @@
--[[
| 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
--- @class TimeDelta
--- @operator add:TimeDelta|DateTime
--- @operator sub:TimeDelta|DateTime
--- @operator mul:TimeDelta
--- @operator div:TimeDelta
--- @operator pow:TimeDelta
--- @operator mod:TimeDelta
local meta = {}
meta.__index = meta
meta.MetaName = "TimeDelta"
NikNaks.__metatables["TimeDelta"] = meta
--- @class TimeDeltaModule
--- @operator call:TimeDelta
local TimeDelta = {}
TimeDelta.Milisecond = 0.001
TimeDelta.Second = 1
TimeDelta.Minute = 60
TimeDelta.Hour = 3600
TimeDelta.Day = 86400
TimeDelta.Week = 604800
TimeDelta.Year = 31536000
TimeDelta.Decade = 315359654
TimeDelta.Century = 3153596543
TimeDelta._steps = { "Year", "Day", "Hour", "Minute", "Second", "Milisecond" }
setmetatable( TimeDelta, {
__index = TimeDelta,
__call = function( _, time )
return setmetatable( { time = time }, meta )
end
} )
NikNaks.TimeDelta = TimeDelta
do
local abs = math.abs
local floor = math.floor
local steps = TimeDelta._steps
--- Returns the time as a table
--- @return table
function meta:ToTable()
if self._tab then return self._tab end
local t = {}
local num = abs( self.time )
local f = self.time < 0 and -1 or 1
local isLeapYear = NikNaks.DateTime.IsLeapYear
local function processStep( i )
local step = steps[i]
local value = steps[step]
-- Leap year
if num < value then return end
if i == 1 then -- Since years aren't whole numbers, we need to round the tiniest amount, or floor is going to count down.
local n = 0
local y = TimeDelta.Year
local v = isLeapYear( y ) and 31622400 or 31536000
while num >= v do
local q = 1 * f
if num >= v then
n = n + 1
num = num - v
y = y + q
v = isLeapYear( y ) and 31622400 or 31536000
else
break
end
end
t[s] = n * f
else
local n = floor( num / v )
t[s] = n * f
num = num - v * n
end
end
for i = 1, #steps do
processStep( i )
end
self._tab = t
return t
end
end
-- Getters
do
--- Generic getter function to get the time amount of the given time type
--- @param key string The name of the time type to get
--- @return number
function meta:_getter( key )
return self.time / TimeDelta[key]
end
function meta:GetMiliseconds() return self:_getter( "Milisecond" ) end
function meta:GetSeconds() return self:_getter( "Second" ) end
function meta:GetMinutes() return self:_getter( "Minute" ) end
function meta:GetHours() return self:_getter( "Hour" ) end
function meta:GetDays() return self:_getter( "Day" ) end
function meta:GetWeeks() return self:_getter( "Week" ) end
function meta:GetMonths() return self:_getter( "Month" ) end
function meta:GetYears() return self:_getter( "Year" ) end
function meta:GetDecades() return self:_getter( "Decade" ) end
function meta:GetCenturies() return self:_getter( "Century" ) end
end
-- Adders
do
--- Generic adder function to add given time amount to the TimeDelta
--- @param key string The name of the time type to add
--- @param num number The amount of time to add
--- @return self TimeDelta
function meta:_adder( key, num )
self.time = self.time + ( num * TimeDelta[key] )
self._tab = nil
return self
end
--- @param n number
function meta:AddMiliseconds( n ) return self:_adder( "Milisecond", n ) end --- @param n number
function meta:AddSeconds( n ) return self:_adder( "Second", n ) end --- @param n number
function meta:AddMinutes( n ) return self:_adder( "Minute", n ) end --- @param n number
function meta:AddHours( n ) return self:_adder( "Hour", n ) end --- @param n number
function meta:AddDays( n ) return self:_adder( "Day", n ) end --- @param n number
function meta:AddWeeks( n ) return self:_adder( "Week", n ) end --- @param n number
function meta:AddMonths( n ) return self:_adder( "Month", n ) end --- @param n number
function meta:AddYears( n ) return self:_adder( "Year", n ) end --- @param n number
function meta:AddDecades( n ) return self:_adder( "Decade", n ) end --- @param n number
function meta:AddCenturies( n ) return self:_adder( "Century", n ) end --- @param n number
end
-- Subtractors
do
--- Generic subtractor function to subtract given time amount from the TimeDelta
--- @param key string The name of the time type to subtract
--- @param num number The amount of time to subtract
--- @return self TimeDelta
function meta:_subtractor( key, num )
self.time = self.time - ( num * TimeDelta[key] )
self._tab = nil
return self
end
function meta:SubMiliseconds( n ) return self:_subtractor( "Milisecond", n ) end --- @param n number
function meta:SubSeconds( n ) return self:_subtractor( "Second", n ) end --- @param n number
function meta:SubMinutes( n ) return self:_subtractor( "Minute", n ) end --- @param n number
function meta:SubHours( n ) return self:_subtractor( "Hour", n ) end --- @param n number
function meta:SubDays( n ) return self:_subtractor( "Day", n ) end --- @param n number
function meta:SubWeeks( n ) return self:_subtractor( "Week", n ) end --- @param n number
function meta:SubMonths( n ) return self:_subtractor( "Month", n ) end --- @param n number
function meta:SubYears( n ) return self:_subtractor( "Year", n ) end --- @param n number
function meta:SubDecades( n ) return self:_subtractor( "Decade", n ) end --- @param n number
function meta:SubCenturies( n ) return self:_subtractor( "Century", n ) end --- @param n number
end
-- ToString
do
local abs = math.abs
local steps = TimeDelta._steps
function meta:__tostring()
local str
local si = 0
local tab = self:ToTable()
local kv = #table.GetKeys( tab )
for i = 1, #steps do
local step = steps[i]
if tab[step] then
si = si + 1
local number = abs( tab[step] )
local middle = ( si == kv and " and " or ", " )
step = number == 1 and step or step .. "s"
if not str then
str = number .. " " .. step
else
str = str .. middle .. number .. " " .. step
end
end
end
return str or "nil"
end
end
function meta:IsNegative()
return self.time < 0
end
function meta:IsPositive()
return self.time >= 0
end
-- Operations
do
--- @param b TimeDelta|number
--- @return TimeDelta|DateTime
function meta:__add( b )
if isnumber( b ) then
return TimeDelta( self.time + b )
end
if self.unix and b.time then
return NikNaks.DateTime( self.unix + b.time )
elseif b.unix and self.time then
return NikNaks.DateTime( b.unix + self.time )
end
end
--- @param b TimeDelta|number
--- @return TimeDelta|DateTime
function meta:__sub( b )
if isnumber( b ) then
return TimeDelta( self.time - b )
end
if self.unix and b.time then
return NikNaks.DateTime( self.unix - b.time )
elseif b.unix and self.time then
return NikNaks.DateTime( b.unix - self.time )
end
end
--- @param b TimeDelta|number
--- @return TimeDelta
function meta:__mul( b )
b = isnumber( b ) and b or b.time
return TimeDelta( self.time * b )
end
--- @param b TimeDelta|number
--- @return TimeDelta
function meta:__div( b )
b = isnumber( b ) and b or b.time
return TimeDelta( self.time / b )
end
--- @param b TimeDelta|number
--- @return TimeDelta
function meta:__pow( b )
b = isnumber( b ) and b or b.time
return TimeDelta( self.time ^ b )
end
--- @param b TimeDelta|number
--- @return TimeDelta
function meta:__mod( b )
b = isnumber( b ) and b or b.time
return TimeDelta( self.time % b )
end
--- @param b TimeDelta
function meta:__eq( b )
return self.time == b.time
end
--- @param b TimeDelta
function meta:__lt( b )
return a.time < b.time
end
--- @param b TimeDelta
function meta.__le( b )
return a.time <= b.time
end
end

View File

@@ -0,0 +1,158 @@
--[[
| 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.
local NikNaks = NikNaks
local tostring, tonumber, tobool, Angle, Vector, string_ToColor = tostring, tonumber, tobool, Angle, Vector, string.ToColor
-- Lua based type fix
-- TODO: Note sure if it should be added
if false then
NikNaks.oldType = type
function NikNaks.isnumber( var )
local mt = getmetatable( var )
if not mt or mt.MetaName ~= "number" then return false end
return true
end
function NikNaks.isstring( var )
local mt = getmetatable( var )
if not mt or mt.MetaName ~= "string" then return false end
return true
end
function NikNaks.istable( var )
if not getmetatable( var ) then return true end
return false
end
function NikNaks.isfunction( var )
local mt = getmetatable( var )
if not mt or mt.MetaName ~= "function" then return false end
return true
end
function NikNaks.isvector( var )
local mt = getmetatable( var )
if not mt or mt.MetaName ~= "Vector" then return false end
return true
end
function NikNaks.isangle( var )
local mt = getmetatable( var )
if not mt or mt.MetaName ~= "Angle" then return false end
return true
end
function NikNaks.isbool( var )
return var == true or var == false or false
end
function NikNaks.isplayer( var )
local mt = getmetatable( var )
if not mt or mt.MetaName ~= "Player" then return false end
return true
end
function NikNaks.isentity( var )
local mt = getmetatable( var )
if not mt or (mt.MetaName ~= "Player" and mt.MetaName ~= "Entity" ) then return false end
return true
end
local function PatchMetaName( var, str )
-- If it has a metatable.
local mt = getmetatable(var)
if mt then -- Make sure the metatable has the metaname
mt.MetaName = str
else
local tab = {["MetaName"] = str}
debug.setmetatable(var, tab)
end
end
PatchMetaName("", "string")
PatchMetaName(1, "number")
PatchMetaName(function() end, "function")
PatchMetaName(coroutine.create(function() end), "thread")
function NikNaks.type( var )
local mt = getmetatable( var )
if mt and mt.MetaName then return mt.MetaName end
return "table"
end
end
--- Same as AccessorFunc, but will make 'Set' functions return self. Allowing you to chain-call.
--- @param tab table
--- @param varname string
--- @param name string
--- @param iForce? number
function NikNaks.AccessorFuncEx( tab, varname, name, iForce )
if not tab then debug.Trace() end
tab[ "Get" .. name ] = function( self ) return self[ varname ] end
if ( iForce == FORCE_STRING ) then
tab[ "Set" .. name ] = function( self, v ) self[ varname ] = tostring( v ) return self end
return end
if ( iForce == FORCE_NUMBER ) then
tab[ "Set" .. name ] = function( self, v ) self[ varname ] = tonumber( v ) return self end
return end
if ( iForce == FORCE_BOOL ) then
tab[ "Set" .. name ] = function( self, v ) self[ varname ] = tobool( v ) return self end
return end
if ( iForce == FORCE_ANGLE ) then
tab[ "Set" .. name ] = function( self, v ) self[ varname ] = Angle( v ) return self end
return end
if ( iForce == FORCE_COLOR ) then
tab[ "Set" .. name ] = function( self, v )
if ( NikNaks.type( v ) == "Vector" ) then self[ varname ] = v:ToColor()
else self[ varname ] = string_ToColor( tostring( v ) ) end
return self
end
return end
if ( iForce == FORCE_VECTOR ) then
tab[ "Set" .. name ] = function( self, v )
if ( IsColor( v ) ) then self[ varname ] = v:ToVector()
else self[ varname ] = Vector( v ) end
return self
end
return end
tab[ "Set" .. name ] = function( self, v ) self[ varname ] = v return self end
end
NikNaks.util = {}
-- Hull
do
--- Returns a HULL_ENUM fitting the hull given.
--- @param vecMin Vector
--- @param vecMax Vector
--- @return number HULL_ENUM
function NikNaks.util.FindHull( vecMin, vecMax )
local wide = max(-vecMin.x, -vecMin.y, vecMax.x, vecMax.y)
local high = vecMax.z - vecMin.z
if wide <= 16 and high <= 8 then
return NikNaks.HULL_TINY_CENTERED
elseif wide <= 24 and high <= 24 then
return NikNaks.HULL_TINY
elseif wide <= 40 and high <= 40 then
return NikNaks.HULL_SMALL_CENTERED
elseif wide <= 36 and high <= 65 then
return NikNaks.HULL_MEDIUM
elseif wide <= 32 and high <= 73 then
return NikNaks.HULL_HUMAN
elseif wide <= 36 and high <= 100 then
return NikNaks.HULL_MEDIUM_TALL
else
return NikNaks.HULL_LARGE
end
end
--- Returns a HULL_ENUM matching the entitys hull.
--- @param entity Entity
--- @return number HULL_ENUM
function NikNaks.util.FindEntityHull( entity )
if entity.GetHull then return entity:GetHull() end
local mi, ma = entity:OBBMins(), entity:OBBMaxs()
return FindHull( mi, ma )
end
end