Files
wnsrc/gamemodes/helix/gamemode/core/sh_util.lua
lifestorm 73479cff9e Upload
2024-08-04 22:55:00 +03:00

1167 lines
32 KiB
Lua

--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--- Various useful helper functions.
-- @module ix.util
ix.type = ix.type or {
[2] = "string",
[4] = "text",
[8] = "number",
[16] = "player",
[32] = "steamid",
[64] = "character",
[128] = "bool",
[1024] = "color",
[2048] = "vector",
string = 2,
text = 4,
number = 8,
player = 16,
steamid = 32,
character = 64,
bool = 128,
color = 1024,
vector = 2048,
optional = 256,
array = 512
}
ix.blurRenderQueue = {}
--- Includes a lua file based on the prefix of the file. This will automatically call `include` and `AddCSLuaFile` based on the
-- current realm. This function should always be called shared to ensure that the client will receive the file from the server.
-- @realm shared
-- @string fileName Path of the Lua file to include. The path is relative to the file that is currently running this function
-- @string[opt] realm Realm that this file should be included in. You should usually ignore this since it
-- will be automatically be chosen based on the `SERVER` and `CLIENT` globals. This value should either be `"server"` or
-- `"client"` if it is filled in manually
function ix.util.Include(fileName, realm)
if (!fileName) then
error("[Helix] No file name specified for including.")
end
-- Only include server-side if we're on the server.
if ((realm == "server" or fileName:find("sv_")) and SERVER) then
return include(fileName)
-- Shared is included by both server and client.
elseif (realm == "shared" or fileName:find("shared.lua") or fileName:find("sh_")) then
if (SERVER) then
-- Send the file to the client if shared so they can run it.
AddCSLuaFile(fileName)
end
return include(fileName)
-- File is sent to client, included on client.
elseif (realm == "client" or fileName:find("cl_")) then
if (SERVER) then
AddCSLuaFile(fileName)
else
return include(fileName)
end
end
end
--- Includes multiple files in a directory.
-- @realm shared
-- @string directory Directory to include files from
-- @bool[opt] bFromLua Whether or not to search from the base `lua/` folder, instead of contextually basing from `schema/`
-- or `gamemode/`
-- @see ix.util.Include
function ix.util.IncludeDir(directory, bFromLua)
-- By default, we include relatively to Helix.
local baseDir = "helix"
-- If we're in a schema, include relative to the schema.
if (Schema and Schema.folder and Schema.loading) then
baseDir = Schema.folder.."/schema/"
else
baseDir = baseDir.."/gamemode/"
end
-- Find all of the files within the directory.
for _, v in ipairs(file.Find((bFromLua and "" or baseDir)..directory.."/*.lua", "LUA")) do
-- Include the file from the prefix.
ix.util.Include(directory.."/"..v)
end
end
--- Removes the realm prefix from a file name. The returned string will be unchanged if there is no prefix found.
-- @realm shared
-- @string name String to strip prefix from
-- @treturn string String stripped of prefix
-- @usage print(ix.util.StripRealmPrefix("sv_init.lua"))
-- > init.lua
function ix.util.StripRealmPrefix(name)
local prefix = name:sub(1, 3)
return (prefix == "sh_" or prefix == "sv_" or prefix == "cl_") and name:sub(4) or name
end
--- Returns `true` if the given input is a color table. This is necessary since the engine `IsColor` function only checks for
-- color metatables - which are not used for regular Lua color types.
-- @realm shared
-- @param input Input to check
-- @treturn bool Whether or not the input is a color
function ix.util.IsColor(input)
return istable(input) and
isnumber(input.a) and isnumber(input.g) and isnumber(input.b) and (input.a and isnumber(input.a) or input.a == nil)
end
--- Returns a dimmed version of the given color by the given scale.
-- @realm shared
-- @color color Color to dim
-- @number multiplier What to multiply the red, green, and blue values by
-- @number[opt=255] alpha Alpha to use in dimmed color
-- @treturn color Dimmed color
-- @usage print(ix.util.DimColor(Color(100, 100, 100, 255), 0.5))
-- > 50 50 50 255
function ix.util.DimColor(color, multiplier, alpha)
return Color(color.r * multiplier, color.g * multiplier, color.b * multiplier, alpha or 255)
end
--- Sanitizes an input value with the given type. This function ensures that a valid type is always returned. If a valid value
-- could not be found, it will return the default value for the type. This only works for simple types - e.g it does not work
-- for player, character, or Steam ID types.
-- @realm shared
-- @ixtype type Type to check for
-- @param input Value to sanitize
-- @return Sanitized value
-- @see ix.type
-- @usage print(ix.util.SanitizeType(ix.type.number, "123"))
-- > 123
-- print(ix.util.SanitizeType(ix.type.bool, 1))
-- > true
function ix.util.SanitizeType(type, input)
if (type == ix.type.string) then
return tostring(input)
elseif (type == ix.type.text) then
return tostring(input)
elseif (type == ix.type.number) then
return tonumber(input or 0) or 0
elseif (type == ix.type.bool) then
return tobool(input)
elseif (type == ix.type.color) then
return istable(input) and
Color(tonumber(input.r) or 255, tonumber(input.g) or 255, tonumber(input.b) or 255, tonumber(input.a) or 255) or
color_white
elseif (type == ix.type.vector) then
return isvector(input) and input or vector_origin
elseif (type == ix.type.array) then
return input
else
error("attempted to sanitize " .. (ix.type[type] and ("invalid type " .. ix.type[type]) or "unknown type " .. type))
end
end
do
local typeMap = {
string = ix.type.string,
number = ix.type.number,
Player = ix.type.player,
boolean = ix.type.bool,
Vector = ix.type.vector
}
local tableMap = {
[ix.type.character] = function(value)
return getmetatable(value) == ix.meta.character
end,
[ix.type.color] = function(value)
return ix.util.IsColor(value)
end,
[ix.type.steamid] = function(value)
return isstring(value) and (value:match("STEAM_(%d+):(%d+):(%d+)")) != nil
end
}
--- Returns the `ix.type` of the given value.
-- @realm shared
-- @param value Value to get the type of
-- @treturn ix.type Type of value
-- @see ix.type
-- @usage print(ix.util.GetTypeFromValue("hello"))
-- > 2 -- i.e the value of ix.type.string
function ix.util.GetTypeFromValue(value)
local result = typeMap[type(value)]
if (result) then
return result
end
if (istable(value)) then
for k, v in pairs(tableMap) do
if (v(value)) then
return k
end
end
end
end
end
function ix.util.Bind(self, callback)
return function(_, ...)
return callback(self, ...)
end
end
-- Returns the address:port of the server.
function ix.util.GetAddress()
local address = tonumber(GetConVarString("hostip"))
if (!address) then
return "127.0.0.1"..":"..GetConVarString("hostport")
end
local ip = {}
ip[1] = bit.rshift(bit.band(address, 0xFF000000), 24)
ip[2] = bit.rshift(bit.band(address, 0x00FF0000), 16)
ip[3] = bit.rshift(bit.band(address, 0x0000FF00), 8)
ip[4] = bit.band(address, 0x000000FF)
return table.concat(ip, ".")..":"..GetConVarString("hostport")
end
--- Returns a cached copy of the given material, or creates and caches one if it doesn't exist. This is a quick helper function
-- if you aren't locally storing a `Material()` call.
-- @realm shared
-- @string materialPath Path to the material
-- @treturn[1] material The cached material
-- @treturn[2] nil If the material doesn't exist in the filesystem
function ix.util.GetMaterial(materialPath)
-- Cache the material.
ix.util.cachedMaterials = ix.util.cachedMaterials or {}
ix.util.cachedMaterials[materialPath] = ix.util.cachedMaterials[materialPath] or Material(materialPath)
return ix.util.cachedMaterials[materialPath]
end
--- Attempts to find a player by matching their name or Steam ID.
-- @realm shared
-- @string identifier Search query
-- @bool[opt=false] bAllowPatterns Whether or not to accept Lua patterns in `identifier`
-- @treturn player Player that matches the given search query - this will be `nil` if a player could not be found
function ix.util.FindPlayer(identifier, bAllowPatterns)
if !identifier then
return
end
if (string.find(identifier, "STEAM_(%d+):(%d+):(%d+)")) then
return player.GetBySteamID(identifier)
end
if (!bAllowPatterns) then
identifier = string.PatternSafe(identifier)
end
for _, v in ipairs(player.GetAll()) do
if (ix.util.StringMatches(v:Name(), identifier)) then
return v
end
end
end
--- Checks to see if two strings are equivalent using a fuzzy manner. Both strings will be lowered, and will return `true` if
-- the strings are identical, or if `b` is a substring of `a`.
-- @realm shared
-- @string a First string to check
-- @string b Second string to check
-- @treturn bool Whether or not the strings are equivalent
function ix.util.StringMatches(a, b)
if (a and b) then
local a2, b2 = a:utf8lower(), b:utf8lower()
-- Check if the actual letters match.
if (a == b) then return true end
if (a2 == b2) then return true end
-- Be less strict and search.
if (a:find(b)) then return true end
if (a2:find(b2)) then return true end
end
return false
end
--- Returns a string that has the named arguments in the format string replaced with the given arguments.
-- @realm shared
-- @string format Format string
-- @tparam tab|... Arguments to pass to the formatted string. If passed a table, it will use that table as the lookup table for
-- the named arguments. If passed multiple arguments, it will replace the arguments in the string in order.
-- @usage print(ix.util.FormatStringNamed("Hi, my name is {name}.", {name = "Bobby"}))
-- > Hi, my name is Bobby.
-- @usage print(ix.util.FormatStringNamed("Hi, my name is {name}.", "Bobby"))
-- > Hi, my name is Bobby.
function ix.util.FormatStringNamed(format, ...)
local arguments = {...}
local bArray = false -- Whether or not the input has numerical indices or named ones
local input
-- If the first argument is a table, we can assumed it's going to specify which
-- keys to fill out. Otherwise we'll fill in specified arguments in order.
if (istable(arguments[1])) then
input = arguments[1]
else
input = arguments
bArray = true
end
local i = 0
local result = format:gsub("{(%w-)}", function(word)
i = i + 1
return tostring((bArray and input[i] or input[word]) or word)
end)
return result
end
do
local upperMap = {
["ooc"] = true,
["looc"] = true,
["afk"] = true,
["url"] = true
}
--- Returns a string that is the given input with spaces in between each CamelCase word. This function will ignore any words
-- that do not begin with a capital letter. The words `ooc`, `looc`, `afk`, and `url` will be automatically transformed
-- into uppercase text. This will not capitalize non-ASCII letters due to limitations with Lua's pattern matching.
-- @realm shared
-- @string input String to expand
-- @bool[opt=false] bNoUpperFirst Whether or not to avoid capitalizing the first character. This is useful for lowerCamelCase
-- @treturn string Expanded CamelCase string
-- @usage print(ix.util.ExpandCamelCase("HelloWorld"))
-- > Hello World
function ix.util.ExpandCamelCase(input, bNoUpperFirst)
input = bNoUpperFirst and input or input:utf8sub(1, 1):utf8upper() .. input:utf8sub(2)
-- extra parentheses to select first return value of gsub
return string.TrimRight((input:gsub("%u%l+", function(word)
if (upperMap[word:utf8lower()]) then
word = word:utf8upper()
end
return word .. " "
end)))
end
end
function ix.util.GridVector(vec, gridSize)
if (gridSize <= 0) then
gridSize = 1
end
for i = 1, 3 do
vec[i] = vec[i] / gridSize
vec[i] = math.Round(vec[i])
vec[i] = vec[i] * gridSize
end
return vec
end
do
local i
local value
local character
local function iterator(table)
repeat
i = i + 1
value = table[i]
character = value and value:GetCharacter()
until character or value == nil
return value, character
end
--- Returns an iterator for characters. The resulting key/values will be a player and their corresponding characters. This
-- iterator skips over any players that do not have a valid character loaded.
-- @realm shared
-- @treturn Iterator
-- @usage for client, character in ix.util.GetCharacters() do
-- print(client, character)
-- end
-- > Player [1][Bot01] character[1]
-- > Player [2][Bot02] character[2]
-- -- etc.
function ix.util.GetCharacters()
i = 0
return iterator, player.GetAll()
end
end
if (CLIENT) then
local blur = ix.util.GetMaterial("pp/blurscreen")
local surface = surface
--- Blurs the content underneath the given panel. This will fall back to a simple darkened rectangle if the player has
-- blurring disabled.
-- @realm client
-- @tparam panel panel Panel to draw the blur for
-- @number[opt=5] amount Intensity of the blur. This should be kept between 0 and 10 for performance reasons
-- @number[opt=0.2] passes Quality of the blur. This should be kept as default
-- @number[opt=255] alpha Opacity of the blur
-- @usage function PANEL:Paint(width, height)
-- ix.util.DrawBlur(self)
-- end
function ix.util.DrawBlur(panel, amount, passes, alpha)
amount = amount or 5
if (ix.option.Get("cheapBlur", false)) then
surface.SetDrawColor(50, 50, 50, alpha or (amount * 20))
surface.DrawRect(0, 0, panel:GetWide(), panel:GetTall())
else
surface.SetMaterial(blur)
surface.SetDrawColor(255, 255, 255, alpha or 255)
local x, y = panel:LocalToScreen(0, 0)
for i = -(passes or 0.2), 1, 0.2 do
-- Do things to the blur material to make it blurry.
blur:SetFloat("$blur", i * amount)
blur:Recompute()
-- Draw the blur material over the screen.
render.UpdateScreenEffectTexture()
surface.DrawTexturedRect(x * -1, y * -1, ScrW(), ScrH())
end
end
end
--- Draws a blurred rectangle with the given position and bounds. This shouldn't be used for panels, see `ix.util.DrawBlur`
-- instead.
-- @realm client
-- @number x X-position of the rectangle
-- @number y Y-position of the rectangle
-- @number width Width of the rectangle
-- @number height Height of the rectangle
-- @number[opt=5] amount Intensity of the blur. This should be kept between 0 and 10 for performance reasons
-- @number[opt=0.2] passes Quality of the blur. This should be kept as default
-- @number[opt=255] alpha Opacity of the blur
-- @usage hook.Add("HUDPaint", "MyHUDPaint", function()
-- ix.util.DrawBlurAt(0, 0, ScrW(), ScrH())
-- end)
function ix.util.DrawBlurAt(x, y, width, height, amount, passes, alpha)
amount = amount or 5
if (ix.option.Get("cheapBlur", false)) then
surface.SetDrawColor(30, 30, 30, amount * 20)
surface.DrawRect(x, y, width, height)
else
surface.SetMaterial(blur)
surface.SetDrawColor(255, 255, 255, alpha or 255)
local scrW, scrH = ScrW(), ScrH()
local x2, y2 = x / scrW, y / scrH
local w2, h2 = (x + width) / scrW, (y + height) / scrH
for i = -(passes or 0.2), 1, 0.2 do
blur:SetFloat("$blur", i * amount)
blur:Recompute()
render.UpdateScreenEffectTexture()
surface.DrawTexturedRectUV(x, y, width, height, x2, y2, w2, h2)
end
end
end
--- Pushes a 3D2D blur to be rendered in the world. The draw function will be called next frame in the
-- `PostDrawOpaqueRenderables` hook.
-- @realm client
-- @func drawFunc Function to call when it needs to be drawn
function ix.util.PushBlur(drawFunc)
ix.blurRenderQueue[#ix.blurRenderQueue + 1] = drawFunc
end
--- Draws some text with a shadow.
-- @realm client
-- @string text Text to draw
-- @number x X-position of the text
-- @number y Y-position of the text
-- @color color Color of the text to draw
-- @number[opt=TEXT_ALIGN_LEFT] alignX Horizontal alignment of the text, using one of the `TEXT_ALIGN_*` constants
-- @number[opt=TEXT_ALIGN_LEFT] alignY Vertical alignment of the text, using one of the `TEXT_ALIGN_*` constants
-- @string[opt="ixGenericFont"] font Font to use for the text
-- @number[opt=color.a * 0.575] alpha Alpha of the shadow
function ix.util.DrawText(text, x, y, color, alignX, alignY, font, alpha)
color = color or color_white
return draw.TextShadow({
text = text,
font = font or "ixGenericFont",
pos = {x, y},
color = color,
xalign = alignX or TEXT_ALIGN_LEFT,
yalign = alignY or TEXT_ALIGN_LEFT
}, 1, alpha or (color.a * 0.575))
end
--- Wraps text so it does not pass a certain width. This function will try and break lines between words if it can,
-- otherwise it will break a word if it's too long.
-- @realm client
-- @string text Text to wrap
-- @number maxWidth Maximum allowed width in pixels
-- @string[opt="ixChatFont"] font Font to use for the text
function ix.util.WrapText(text, maxWidth, font)
font = font or "ixChatFont"
surface.SetFont(font)
local words = string.Explode("%s", text, true)
local lines = {}
local line = ""
local lineWidth = 0 -- luacheck: ignore 231
-- we don't need to calculate wrapping if we're under the max width
if (surface.GetTextSize(text) <= maxWidth) then
return {text}
end
for i = 1, #words do
local word = words[i]
local wordWidth = surface.GetTextSize(word)
-- this word is very long so we have to split it by character
if (wordWidth > maxWidth) then
local newWidth
for i2 = 1, string.len(word) do
local character = word[i2]
newWidth = surface.GetTextSize(line .. character)
-- if current line + next character is too wide, we'll shove the next character onto the next line
if (newWidth > maxWidth) then
lines[#lines + 1] = line
line = ""
end
line = line .. character
end
lineWidth = newWidth
continue
end
local newLine = line .. " " .. word
local newWidth = surface.GetTextSize(newLine)
if (newWidth > maxWidth) then
-- adding this word will bring us over the max width
lines[#lines + 1] = line
line = word
lineWidth = wordWidth
else
-- otherwise we tack on the new word and continue
line = newLine
lineWidth = newWidth
end
end
if (line != "") then
lines[#lines + 1] = line
end
return lines
end
local cos, sin, abs, rad1, log, pow = math.cos, math.sin, math.abs, math.rad, math.log, math.pow
-- arc drawing functions
-- by bobbleheadbob
-- https://facepunch.com/showthread.php?t=1558060
function ix.util.DrawArc(cx, cy, radius, thickness, startang, endang, roughness, color)
surface.SetDrawColor(color)
ix.util.DrawPrecachedArc(ix.util.PrecacheArc(cx, cy, radius, thickness, startang, endang, roughness))
end
function ix.util.DrawPrecachedArc(arc) -- Draw a premade arc.
for _, v in ipairs(arc) do
surface.DrawPoly(v)
end
end
function ix.util.PrecacheArc(cx, cy, radius, thickness, startang, endang, roughness)
local quadarc = {}
-- Correct start/end ang
startang = startang or 0
endang = endang or 0
-- Define step
-- roughness = roughness or 1
local diff = abs(startang - endang)
local smoothness = log(diff, 2) / 2
local step = diff / (pow(2, smoothness))
if startang > endang then
step = abs(step) * -1
end
-- Create the inner circle's points.
local inner = {}
local outer = {}
local ct = 1
local r = radius - thickness
for deg = startang, endang, step do
local rad = rad1(deg)
local cosrad, sinrad = cos(rad), sin(rad) --calculate sin, cos
local ox, oy = cx + (cosrad * r), cy + (-sinrad * r) --apply to inner distance
inner[ct] = {
x = ox,
y = oy,
u = (ox - cx) / radius + .5,
v = (oy - cy) / radius + .5
}
local ox2, oy2 = cx + (cosrad * radius), cy + (-sinrad * radius) --apply to outer distance
outer[ct] = {
x = ox2,
y = oy2,
u = (ox2 - cx) / radius + .5,
v = (oy2 - cy) / radius + .5
}
ct = ct + 1
end
-- QUAD the points.
for tri = 1, ct do
local p1, p2, p3, p4
local t = tri + 1
p1 = outer[tri]
p2 = outer[t]
p3 = inner[t]
p4 = inner[tri]
quadarc[tri] = {p1, p2, p3, p4}
end
-- Return a table of triangles to draw.
return quadarc
end
--- Resets all stencil values to known good (i.e defaults)
-- @realm client
function ix.util.ResetStencilValues()
render.SetStencilWriteMask(0xFF)
render.SetStencilTestMask(0xFF)
render.SetStencilReferenceValue(0)
render.SetStencilCompareFunction(STENCIL_ALWAYS)
render.SetStencilPassOperation(STENCIL_KEEP)
render.SetStencilFailOperation(STENCIL_KEEP)
render.SetStencilZFailOperation(STENCIL_KEEP)
render.ClearStencil()
end
-- luacheck: globals derma
-- Alternative to SkinHook that allows you to pass more arguments to skin methods
function derma.SkinFunc(name, panel, a, b, c, d, e, f, g)
local skin = (ispanel(panel) and IsValid(panel)) and panel:GetSkin() or derma.GetDefaultSkin()
if (!skin) then
return
end
local func = skin[name]
if (!func) then
return
end
return func(skin, panel, a, b, c, d, e, f, g)
end
-- Alternative to Color that retrieves from the SKIN.Colours table
function derma.GetColor(name, panel, default)
default = default or ix.config.Get("color")
local skin = panel:GetSkin()
if (!skin) then
return default
end
return skin.Colours[name] or default
end
hook.Add("OnScreenSizeChanged", "ix.OnScreenSizeChanged", function(oldWidth, oldHeight)
hook.Run("ScreenResolutionChanged", oldWidth, oldHeight)
end)
end
-- Vector extension, courtesy of code_gs (fixed by M!NT)
do
local VECTOR = FindMetaTable("Vector")
local CrossProduct = VECTOR.Cross
local right = Vector(0, -1, 0)
function VECTOR:Right(vUp)
if (self[1] == 0 and self[2] == 0) then
return right
end
if (vUp == nil) then
vUp = vector_up
end
local vRet = CrossProduct(self, vUp)
vRet:Normalize()
return vRet
end
function VECTOR:Up(vUp)
if (self[1] == 0 and self[2] == 0) then return Vector(-self[3], 0, 0) end
if (vUp == nil) then
vUp = vector_up
end
local vRet = CrossProduct(self, vUp)
vRet = CrossProduct(vRet, self)
vRet:Normalize()
return vRet
end
end
-- luacheck: globals FCAP_IMPULSE_USE FCAP_CONTINUOUS_USE FCAP_ONOFF_USE
-- luacheck: globals FCAP_DIRECTIONAL_USE FCAP_USE_ONGROUND FCAP_USE_IN_RADIUS
FCAP_IMPULSE_USE = 0x00000010
FCAP_CONTINUOUS_USE = 0x00000020
FCAP_ONOFF_USE = 0x00000040
FCAP_DIRECTIONAL_USE = 0x00000080
FCAP_USE_ONGROUND = 0x00000100
FCAP_USE_IN_RADIUS = 0x00000200
function ix.util.IsUseableEntity(entity, requiredCaps)
if (IsValid(entity)) then
local caps = entity:ObjectCaps()
if (bit.band(caps, bit.bor(FCAP_IMPULSE_USE, FCAP_CONTINUOUS_USE, FCAP_ONOFF_USE, FCAP_DIRECTIONAL_USE))) then
if (bit.band(caps, requiredCaps) == requiredCaps) then
return true
end
end
end
end
do
local function IntervalDistance(x, x0, x1)
-- swap so x0 < x1
if (x0 > x1) then
local tmp = x0
x0 = x1
x1 = tmp
end
if (x < x0) then
return x0-x
elseif (x > x1) then
return x - x1
end
return 0
end
local NUM_TANGENTS = 8
local tangents = {0, 1, 0.57735026919, 0.3639702342, 0.267949192431, 0.1763269807, -0.1763269807, -0.267949192431}
local traceMin = Vector(-16, -16, -16)
local traceMax = Vector(16, 16, 16)
function ix.util.FindUseEntity(player, origin, forward)
local tr
local up = forward:Up()
-- Search for objects in a sphere (tests for entities that are not solid, yet still useable)
local searchCenter = origin
-- NOTE: Some debris objects are useable too, so hit those as well
-- A button, etc. can be made out of clip brushes, make sure it's +useable via a traceline, too.
local useableContents = bit.bor(MASK_SOLID, CONTENTS_DEBRIS, CONTENTS_PLAYERCLIP)
-- UNDONE: Might be faster to just fold this range into the sphere query
local pObject
local nearestDist = 1e37
-- try the hit entity if there is one, or the ground entity if there isn't.
local pNearest = NULL
for i = 1, NUM_TANGENTS do
if (i == 0) then
tr = util.TraceLine({
start = searchCenter,
endpos = searchCenter + forward * 1024,
mask = useableContents,
filter = player
})
tr.EndPos = searchCenter + forward * 1024
else
local down = forward - tangents[i] * up
down:Normalize()
tr = util.TraceHull({
start = searchCenter,
endpos = searchCenter + down * 72,
mins = traceMin,
maxs = traceMax,
mask = useableContents,
filter = player
})
tr.EndPos = searchCenter + down * 72
end
pObject = tr.Entity
local bUsable = ix.util.IsUseableEntity(pObject, 0)
while (IsValid(pObject) and !bUsable and pObject:GetMoveParent()) do
pObject = pObject:GetMoveParent()
bUsable = ix.util.IsUseableEntity(pObject, 0)
end
if (bUsable) then
local delta = tr.EndPos - tr.StartPos
local centerZ = origin.z - player:WorldSpaceCenter().z
delta.z = IntervalDistance(tr.EndPos.z, centerZ - player:OBBMins().z, centerZ + player:OBBMaxs().z)
local dist = delta:Length()
if (dist < 80) then
pNearest = pObject
-- if this is directly under the cursor just return it now
if (i == 0) then
return pObject
end
end
end
end
-- check ground entity first
-- if you've got a useable ground entity, then shrink the cone of this search to 45 degrees
-- otherwise, search out in a 90 degree cone (hemisphere)
if (IsValid(player:GetGroundEntity()) and ix.util.IsUseableEntity(player:GetGroundEntity(), FCAP_USE_ONGROUND)) then
pNearest = player:GetGroundEntity()
end
if (IsValid(pNearest)) then
-- estimate nearest object by distance from the view vector
local point = pNearest:NearestPoint(searchCenter)
nearestDist = util.DistanceToLine(searchCenter, forward, point)
end
for _, v in pairs(ents.FindInSphere(searchCenter, 80)) do
if (!ix.util.IsUseableEntity(v, FCAP_USE_IN_RADIUS)) then
continue
end
-- see if it's more roughly in front of the player than previous guess
local point = v:NearestPoint(searchCenter)
local dir = point - searchCenter
dir:Normalize()
local dot = dir:Dot(forward)
-- Need to be looking at the object more or less
if (dot < 0.8) then
continue
end
local dist = util.DistanceToLine(searchCenter, forward, point)
if (dist < nearestDist) then
-- Since this has purely been a radius search to this point, we now
-- make sure the object isn't behind glass or a grate.
local trCheckOccluded = {}
util.TraceLine({
start = searchCenter,
endpos = point,
mask = useableContents,
filter = player,
output = trCheckOccluded
})
if (trCheckOccluded.fraction == 1.0 or trCheckOccluded.Entity == v) then
pNearest = v
nearestDist = dist
end
end
end
return pNearest
end
end
ALWAYS_RAISED = {}
ALWAYS_RAISED["weapon_physgun"] = true
ALWAYS_RAISED["gmod_tool"] = true
ALWAYS_RAISED["ix_poshelper"] = true
function ix.util.FindEmptySpace(entity, filter, spacing, size, height, tolerance)
spacing = spacing or 32
size = size or 3
height = height or 36
tolerance = tolerance or 5
local position = entity:GetPos()
local mins, maxs = Vector(-spacing * 0.5, -spacing * 0.5, 0), Vector(spacing * 0.5, spacing * 0.5, height)
local output = {}
for x = -size, size do
for y = -size, size do
local origin = position + Vector(x * spacing, y * spacing, 0)
local data = {}
data.start = origin + mins + Vector(0, 0, tolerance)
data.endpos = origin + maxs
data.filter = filter or entity
local trace = util.TraceLine(data)
data.start = origin + Vector(-maxs.x, -maxs.y, tolerance)
data.endpos = origin + Vector(mins.x, mins.y, height)
local trace2 = util.TraceLine(data)
if (trace.StartSolid or trace.Hit or trace2.StartSolid or trace2.Hit or !util.IsInWorld(origin)) then
continue
end
output[#output + 1] = origin
end
end
table.sort(output, function(a, b)
return a:Distance(position) < b:Distance(position)
end)
return output
end
-- Time related stuff.
do
--- Gets the current time in the UTC time-zone.
-- @realm shared
-- @treturn number Current time in UTC
function ix.util.GetUTCTime()
local date = os.date("!*t")
local localDate = os.date("*t")
localDate.isdst = false
return os.difftime(os.time(date), os.time(localDate))
end
-- Setup for time strings.
local TIME_UNITS = {}
TIME_UNITS["s"] = 1 -- Seconds
TIME_UNITS["m"] = 60 -- Minutes
TIME_UNITS["h"] = 3600 -- Hours
TIME_UNITS["d"] = TIME_UNITS["h"] * 24 -- Days
TIME_UNITS["w"] = TIME_UNITS["d"] * 7 -- Weeks
TIME_UNITS["mo"] = TIME_UNITS["d"] * 30 -- Months
TIME_UNITS["y"] = TIME_UNITS["d"] * 365 -- Years
--- Gets the amount of seconds from a given formatted string. If no time units are specified, it is assumed minutes.
-- The valid values are as follows:
--
-- - `s` - Seconds
-- - `m` - Minutes
-- - `h` - Hours
-- - `d` - Days
-- - `w` - Weeks
-- - `mo` - Months
-- - `y` - Years
-- @realm shared
-- @string text Text to interpret a length of time from
-- @treturn[1] number Amount of seconds from the length interpreted from the given string
-- @treturn[2] 0 If the given string does not have a valid time
-- @usage print(ix.util.GetStringTime("5y2d7w"))
-- > 162086400 -- 5 years, 2 days, 7 weeks
function ix.util.GetStringTime(text)
local minutes = tonumber(text)
if (minutes) then
return math.abs(minutes * 60)
end
local time = 0
for amount, unit in text:lower():gmatch("(%d+)(%a+)") do
amount = tonumber(amount)
if (amount and TIME_UNITS[unit]) then
time = time + math.abs(amount * TIME_UNITS[unit])
end
end
return time
end
end
--[[
Credit to TFA for figuring this mess out.
Original: https://steamcommunity.com/sharedfiles/filedetails/?id=903541818
]]
if (system.IsLinux()) then
local cache = {}
-- Helper Functions
local function GetSoundPath(path, gamedir)
if (!gamedir) then
path = "sound/" .. path
gamedir = "GAME"
end
return path, gamedir
end
local function f_IsWAV(f)
f:Seek(8)
return f:Read(4) == "WAVE"
end
-- WAV functions
local function f_SampleDepth(f)
f:Seek(34)
local bytes = {}
for i = 1, 2 do
bytes[i] = f:ReadByte(1)
end
local num = bit.lshift(bytes[2], 8) + bit.lshift(bytes[1], 0)
return num
end
local function f_SampleRate(f)
f:Seek(24)
local bytes = {}
for i = 1, 4 do
bytes[i] = f:ReadByte(1)
end
local num = bit.lshift(bytes[4], 24) + bit.lshift(bytes[3], 16) + bit.lshift(bytes[2], 8) + bit.lshift(bytes[1], 0)
return num
end
local function f_Channels(f)
f:Seek(22)
local bytes = {}
for i = 1, 2 do
bytes[i] = f:ReadByte(1)
end
local num = bit.lshift(bytes[2], 8) + bit.lshift(bytes[1], 0)
return num
end
local function f_Duration(f)
return (f:Size() - 44) / (f_SampleDepth(f) / 8 * f_SampleRate(f) * f_Channels(f))
end
ixSoundDuration = ixSoundDuration or SoundDuration -- luacheck: globals ixSoundDuration
function SoundDuration(str) -- luacheck: globals SoundDuration
local path, gamedir = GetSoundPath(str)
local f = file.Open(path, "rb", gamedir)
if (!f) then return 0 end --Return nil on invalid files
local ret
if (cache[str]) then
ret = cache[str]
elseif (f_IsWAV(f)) then
ret = f_Duration(f)
else
ret = ixSoundDuration(str)
end
f:Close()
return ret
end
end
local ADJUST_SOUND = SoundDuration("npc/metropolice/pain1.wav") > 0 and "" or "../../hl2/sound/"
--- Emits sounds one after the other from an entity.
-- @realm shared
-- @entity entity Entity to play sounds from
-- @tab sounds Sound paths to play
-- @number delay[opt=0] How long to wait before starting to play the sounds
-- @number spacing[opt=0.1] How long to wait between playing each sound
-- @number volume[opt=75] The sound level of each sound
-- @number pitch[opt=100] Pitch percentage of each sound
-- @treturn number How long the entire sequence of sounds will take to play
function ix.util.EmitQueuedSounds(entity, sounds, delay, spacing, volume, pitch)
-- Let there be a delay before any sound is played.
delay = delay or 0
spacing = spacing or 0.1
-- Loop through all of the sounds.
for _, v in ipairs(sounds) do
local postSet, preSet = 0, 0
-- Determine if this sound has special time offsets.
if (istable(v)) then
postSet, preSet = v[2] or 0, v[3] or 0
v = v[1]
end
-- Get the length of the sound.
local length = SoundDuration(ADJUST_SOUND..v)
-- If the sound has a pause before it is played, add it here.
delay = delay + preSet
-- Have the sound play in the future.
timer.Simple(delay, function()
-- Check if the entity still exists and play the sound.
if (IsValid(entity)) then
entity:EmitSound(v, volume, pitch)
end
end)
-- Add the delay for the next sound.
delay = delay + length + postSet + spacing
end
-- Return how long it took for the whole thing.
return delay
end
function ix.util.ColorLerp(t, from, to)
return Color(
Lerp(t, from.r, to.r),
Lerp(t, from.g, to.g),
Lerp(t, from.b, to.b),
Lerp(t, from.a, to.a)
)
end
ix.util.Include("helix/gamemode/core/meta/sh_entity.lua")
ix.util.Include("helix/gamemode/core/meta/sh_player.lua")