mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-17 21:53:46 +03:00
1167 lines
32 KiB
Lua
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")
|