Skip to content

Instantly share code, notes, and snippets.

@RichardB01
Created April 24, 2012 19:03
Show Gist options
  • Save RichardB01/2482747 to your computer and use it in GitHub Desktop.
Save RichardB01/2482747 to your computer and use it in GitHub Desktop.
UtilX - A collab lua gmod function library.
-----------------------------------
-- utilx
-- A project stared by Gbps
-- expanded with community input
-----------------------------------
utilx = {}
utilx.Version = 1.0
--------------------
-- Meta --
--------------------
local meta = FindMetaTable("Player")
if (meta) then
--[[
User: blackops7799
Name: GetUserGroup()
Usage:
if player.GetByID(1):GetUserGroup() == "<group name>" then
-- do something
end
]]
function meta:GetUserGroup()
return self:GetNetworkedString("UserGroup")
end
--[[
User: Python1320
Name: IsStuck() - Taken from Source SDK, doesn't fully work though. Just throwing ideas.
Usage:
<missing>
]]
function meta:IsStuck()
local tracedata = {}
tracedata.start = self:GetPos()
tracedata.endpos = self:GetPos()
tracedata.mask = MASK_PLAYERSOLID or 33636363
tracedata.filter = self
local trace = util.TraceEntity(tracedata,self)
return trace.StartSolid
end
end
---------------------
-- utilx --
---------------------
--[[
User: Kogitsune
Name: InSet(object:value,vararg list ...) - used to determine if value is inside the list of parameters.
Usage:
if utilx.InSet(ent:GetClass(),"prop_door_rotating","player","prop_vehicle_jeep") then
ent:Remove()
end
]]
function utilx.InSet(val,...)
local k, v
for k, v in ipairs{...} do
if v == val then
return true
end
end
return false
end
--[[
User: Kogitsune
Name: HasBit(value,bit) - used to determine if the bit value contains the mask bit.
Usage:
local a,b,c,d,firemodes,mask
a = 0x01
b = 0x02
c = 0x04
d = 0x08
firemodes = a & b & d
if utilx.HasBit(firemodes,c) then
-- do something
end
]]
function utilx.HasBit(value,bit)
return (value & bit) == bit
end
--[[
User: Kogitsune
Name: SimplePointEntity(string:class) - Creates a simple point entity with no special methods (pretty much stolen from thomasfn, I believe)
Usage:
utilx.SimplePointEntity("info_player_zombie")
utilx.SimplePointEntity("info_player_human")
]]
function utilx.SimplePointEntity(class)
local t
t = { }
t.Type = "point"
t.Base = "base_point"
t.Data = {}
function t:SetKeyValue(k,v)
self.Data[k] = v
end
function t:GetKeyValue(k)
return self.Data[k]
end
scripted_ents.Register(t,class)
end
--[[
User: Kogitsune
Name: FastExplode(str:string,str:sep) - Performs a very fast, pattern-based explode on the string, only works with one char as seperator
Usage:
for k,v in ipairs(utilx.FastExplode(file.Read("sometextfile.txt"),"\n")) do
-- do something
end
]]
function utilx.FastExplode(str,sep)
local k,t
t = {}
for k in str:gmatch("[^"..sep.."]+") do
table.insert(t,k)
end
return t
end
--[[
User: Overv
Name: IndexFromValue(table:tbl,string:val) - Quickly get the index of the given value or nil if the value wasn't found
Usage:
local tbl = {"a","b","c","d","e","f","g"}
print(utilx.IndexFromValue(tbl,"f"))
]]
function utilx.IndexFromValue(tbl,val)
local k,v
for k,v in pairs(tbl) do
if (v == val) then return k end
end
return nil
end
--[[
User: stoned (the-stone)
Name: CleanString(string:str) - Makes string usable for filenames
Usage:
file.Write(utilx.CleanString(ply:SteamID())..".txt","file content...")
]]
function utilx.CleanString(str)
str = str:gsub(" ","_")
str = str:gsub("[^%a%d_]","")
return str
end
--[[
User: Carnag3
Name: GetSoundLength(string:strPath) - Gets the sound length
Usage:
local len = utilx.GetSoundLength("sounds/mysong.wav")
print(len)
]]
function utilx.GetSoundLength(strPath)
return string.ToMinutesSeconds(SoundDuration(strPath))
end
--[[
User: Gbps
Name: FindPointsInLine(vector:vec1,vector:vec2) - It returns a table of all the points(vectors) that make up the line between vec1 and vec2.
Usage:
<missing>
]]
function utilx.FindPointsInLine(vec1,vec2)
local ptstbl = {}
for i=1,100 do
ptstbl[i] = LerpVector(i*0.01,vec1,vec2)
end
return ptstbl
end
--[[
User: Gbps
Name: GetPlayerTrace(ply,distance) - In a sense, it's GetPlayerTrace with the added distance argument that is 'missing' from the current ply:GetPlayerTrace()
Usage:
<missing>
]]
function utilx.GetPlayerTrace(ply,distance)
local pos = ply:GetShootPos()
local ang = ply:GetAimVector()
local tracedata = {}
tracedata.start = pos
tracedata.endpos = pos+(ang*distance)
tracedata.filter = ply
local trace = util.TraceLine(tracedata)
return trace
end
--[[
User: slayer3032
Name: AddSteamFriend(steamid) -- Adds the given user to your steam friendlist
Usage:
]]
function utilx.AddSteamFriend(steamid)
local expl = string.Explode(":",steamid)
local serverid,accountid = tonumber(expl[2]),tonumber(expl[3])
local friendid = string.format("765%0.f",accountid * 2 + 61197960265728 + serverid)
http.Get("http://www.garry.tv/go.php?steam://friends/add/"..friendid,"",function() print("User has been added!") end)
end
--[[
User: awatemonosan
Name: AngleOffset(angle:ang1,angle:ang2) - Finds the difference between ang1 and ang2. Because DOT just doesn't always do the job.
Usage:
local off = utilx.AngleOffset(Angle(0,0,0),Angle(0,90,0))
> Angle(0,90,0)
local off = utilx.AngleOffset(Angle(345,270,0),Angle(0,180,0))
> Angle(-15,-90,0)
]]
function utilx.AngleOffset(ang1,ang2)
return Angle((ang1.p+180-ang2.p)%360-180,(ang1.y+180-ang2.y)%360-180,(ang1..r+180-ang2.r)%360-180)
end
--[[
User: thomasfn
Name: AddToTable(tbl,val) / RemoveFromTable(tbl,val) - Secure adds and removes values from a given table
Usage:
<missing>
]]
function utilx.AddToTable(tbl,val)
if (!table.HasValue(tbl,val)) then
table.insert(tbl,val)
end
end
function utilx.RemoveFromTable(tbl,val)
for k,v in pairs(tbl) do
if (v == val) then
table.remove(tbl,k)
return
end
end
end
--[[
User: Overv
Name: getCommand(string:str) / getArguments(string:str)
Usage:
<missing>
]]
function utilx.GetCommand(str)
return str:match("%w+")
end
function utilx.GetArguments(str)
local args = {}
local i = 1
for v in str:gmatch("%S+") do
if i > 1 then table.insert(args,v) end
i = i + 1
end
return args
end
--[[
User: MakeR
Name: Average
Usage:
<missing>
]]
function utilx.Average(...)
local ret, num = 0
for _, num in ipairs(arg) do
ret = ret + num
end
return ret / #arg, ret
end
/*
__ __ ______ ______ __ __ __
/\ \/\ \/\__ _\\__ _\ /\ \ /\ \ /\ \
\ \ \ \ \/_/\ \//_/\ \/ \ \ \ \ `\`\/'/'
\ \ \ \ \ \ \ \ \ \ \ \ \ \ __`\/ > <
\ \ \_\ \ \ \ \ \_\ \__\ \ \_\ \ \/'/\`\
\ \_____\ \ \_\ /\_____\\ \____/ /\_\\ \_\
\/_____/ \/_/ \/_____/ \/___/ \/_/ \/_/
Compiled by Entoros
Function authors listed by respective functions
*/
utilx = {};
/*-------------------------------------------------------------------------------------------------------------------------
utilx.InSet( Object val, ... )
Returns: Bool inSet
Available On: Shared
Description: Gets whether a value is in a list of parameters
Author: Kogistune
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.InSet( val, ... )
local k, v
for k, v in ipairs{ ... } do
if v == val then
return true
end
end
return false
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.SimplePointEntity( String class )
Returns: Entity point_ent
Available On: Server
Description: Creates a simple point entity without any methods
Author: Kogistune
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.SimplePointEntity( class )
local t
t = { }
t.Type = "point"
t.Base = "base_point"
t.Data = { }
function t:SetKeyValue( k, v )
self.Data[ k ] = v
end
function t:GetKeyValue( k )
return self.Data[ k ]
end
scripted_ents.Register( t, class )
return t
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.FastExplode( String str, String separator )
Returns: Table exploded_string
Available On: Shared
Description: Breaks up a string based on a separator
Author: Kogitsune
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.FastExplode( str, sep )
local k, t
t = { }
for k in str:gmatch( "[^" .. sep .. "]+" ) do
table.insert( t, k )
end
return t
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.CleanPath( String path )
Returns: String cleaned_path
Available On: Shared
Description: Makes a string usable in file names
Author: Averice
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.CleanPath(str)
return string.gsub(tostring(str), "[:/\\\"*%?<>]", "_")
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.IndexFromValue( Table tab, Object value )
Returns: Object index
Available On: Shared
Description: Gets the index from a table based on the first occurance of a given value
Author: Overv
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.IndexFromValue( tbl, val )
for k, v in pairs( tbl ) do
if ( v == val ) then return k end
end
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.GetPlayerTrace( Player ply, Number distance )
Returns: Trace tr
Available On: Shared
Description: Gets a trace from the player for a certain distance
Author: Gbps
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.GetPlayerTrace(ply,distance)
local pos = ply:GetShootPos()
local ang = ply:GetAimVector()
local tracedata = {}
tracedata.start = pos
tracedata.endpos = pos+(ang*distance)
tracedata.filter = ply
local trace = util.TraceLine(tracedata)
return trace;
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.AddStreamFriend( String steamid )
Returns: nil
Available On: Client
Description: Adds a friend in steam
Author: slayer3032
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.AddSteamFriend(steamid)
local expl = string.Explode(":", steamid)
local serverid, accountid = tonumber(expl[2]), tonumber(expl[3])
local friendid = string.format("765%0.f", accountid * 2 + 61197960265728 + serverid)
local panel = vgui.Create("HTML")
panel:SetSize(1,1)
panel:OpenURL("http://www.garry.tv/go.php?steam://friends/add/"..friendid)
timer.Simple(10, panel.Remove, panel)
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.IsOccupied( Vector pos )
Returns: Bool isOccupied
Available On: Shared
Description: Checks if something is at a particular position
Author: Gbps
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.IsOccupied(pos)
local trace = nil
local tracedata = {}
tracedata.start = pos
tracedata.endpos = pos
tracedata.mask = MASK_PLAYERSOLID or 33636363
trace = util.TraceEntity( tracedata , self )
return trace.StartSolid
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx VGUI Things
Description: Describe these things
Author: JetBoom
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.WordBox(parent, text, font, textcolor)
local cpanel = vgui.Create("DPanel", parent)
local label = EasyLabel(cpanel, text, font, textcolor)
local tsizex, tsizey = label:GetSize()
cpanel:SetSize(tsizex + 16, tsizey + 8)
label:SetPos(8, (tsizey + 8) * 0.5 - tsizey * 0.5)
cpanel:SetVisible(true)
cpanel:SetMouseInputEnabled(false)
cpanel:SetKeyboardInputEnabled(false)
return cpanel
end
function utilx.EasyLabel(parent, text, font, textcolor)
local dpanel = vgui.Create("DLabel", parent)
if font then
dpanel:SetFont(font or "Default")
end
dpanel:SetText(text)
dpanel:SizeToContents()
if textcolor then
dpanel:SetTextColor(textcolor)
end
dpanel:SetKeyboardInputEnabled(false)
dpanel:SetMouseInputEnabled(false)
return dpanel
end
function utilx.EasyButton(parent, text, xpadding, ypadding)
local dpanel = vgui.Create("DButton", parent)
if textcolor then
dpanel:SetFGColor(textcolor or color_white)
end
if text then
dpanel:SetText(text)
end
dpanel:SizeToContents()
if xpadding then
dpanel:SetWide(dpanel:GetWide() + xpadding * 2)
end
if ypadding then
dpanel:SetTall(dpanel:GetTall() + ypadding * 2)
end
return dpanel
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.DTextDraw( String text, String font, Number x, Number y, Panel parent [, Color col] )
Returns: DPanel pan
Available On: Client
Description: Draws text on a panel with more flexibility than a DLabel
Author: Entoros
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.DTextDraw(text,font,x,y,parent,col)
col = col or color_white
local pan = vgui.Create("DPanel",parent)
pan:SetSize(parent:GetWide(),parent:GetTall())
pan:SetPos(0,0)
pan.Paint = function()
draw.DrawText(text,font,x,y,col)
end
return pan
end
/*-------------------------------------------------------------------------------------------------------------------------
utilx.PlayeCanSee( Player ply, Entity obj )
Returns: Bool isVisible
Available On: Shared
Description: Checks if a player can see an entity
Author: haza55
-------------------------------------------------------------------------------------------------------------------------*/
function utilx.PlayerCanSee(ply, obj)
if not IsValid( ply ) or not ply:IsPlayer() then return false end
if not obj then return false end
local posmin, posmax = 0, 0
if type(obj) == "vector" then
posmin, posmax = obj, obj
else
if !obj:IsValid() then return false end
posmin, posmax = obj:GetPos() + obj:OBBMaxs(), obj:GetPos() + obj:OBBMins()
end
if CLIENT then
return (posmin:ToScreen().visible + posmax:ToScreen().visible) > 0
else
local eye = ply:GetPos() + ply:GetViewOffset()
local tMin = util.TraceLine({start=eye, endpos=posmin})
-- If it didnt hit, it must be visible! And if it did hit, and it hit our Entity then we can see it.
if(!tMin.Hit || (tMin.Hit && tMin.Entity == obj)) then return true end
local tMax = util.TraceLine({start=eye, endpos=posmax})
if(!tMax.Hit || (tMax.Hit && tMax.Entity == obj)) then return true end
return false
end
return false
end
/*-------------------------------------------------------------------------------------------------------------------------
A Whole Bunch of Math Functions
Description: Just read the comments
Author: blob202
-------------------------------------------------------------------------------------------------------------------------*/
-- Performs a linear interpolation between start and end with the factor amount
function utilx.Lerp(start, endval, amount)
return ((1.0 - amount) * start) + (amount * endval)
end
-- Performs a Hermite interpolation (linear with eased inner and outer limits) between start and end with the factor amount
function utilx.Hermite(start, endval, amount)
return utilx.Lerp(start, endval, amount * amount * (3.0 - 2.0 * amount))
end
-- Performs a sinusoidal interpolation, while easing around the end (when the return value approaches 1)
function utilx.Sinerp(start, endval, amount)
return utilx.Lerp(start, endval, math.sin(amount * math.pi * 0.5))
end
-- Performs a cosinusoidal interpolation, while easing around the start (when the return value approaches 0)
function utilx.Coserp(start, endval, amount)
return utilx.Lerp(start, endval, 1.0 - math.cos(amount * math.pi * 0.5))
end
-- Performs a boing-like interpolation, where the end value is initially overshot, and the return value bounces back and fourth the end value before coming to rest
function utilx.Berp(start, endval, amount)
amount = utilx.Clamp(amount, 0.0, 1.0)
amount = (math.sin(amount * math.pi * (0.2 + 2.5 * (amount ^ 3))) * math.pow(1.0 - amount, 2.2) + amount) * (1.0 + (1.2 * (1.0 - amount)))
return start + (endval - start) * amount
end
-- Performs a linear interpolation, but eases the values
function utilx.SmoothStep(x, min, max)
x = Clamp(x, min, max)
v1 = (x-min)/(max-min)
v2 = (x-min)/(max-min)
return -2*v1 * v1 *v1 + 3*v2 * v2
end
-- Performs an approach from the old value to the new value
function utilx.Curve(newvalue, oldvalue, increments)
if (increments > 1) then oldvalue = oldvalue - (oldvalue - newvalue) / increments end
if (increments <= 1) then oldvalue = newvalue end
return oldvalue
end
-- Returns a value between 0 and 1 as if a ball was bouncing and moving X units
function utilx.Bounce(x)
return math.abs(math.sin(6.28*(x+1.0)*(x+1.0)) * (1.0-x))
end
-- Performs a linear interpolation but with respect to circular limits (0 - 360 degrees)
-- NOTE: start and end are expected in units of Degrees (0 - 360)
function utilx.Clerp(start, endval, amount)
min = 0.0;
max = 360.0;
half = math.abs((max - min)/2.0)
retval = 0.0;
diff = 0.0;
if((endval - start) < -half) then
diff = ((max - start)+endval)*amount
retval = start+diff
elseif((endval - start) > half) then
diff = -((max - endval)+start)*amount
retval = start+diff
else
retval = start+(endval-start)*amount
end
return retval
end
-- Performs a positive cumulative approach from current to target using the absolute value of the indicated increment
function utilx.Approach(current, target, increment)
increment = math.abs( increment )
if (current < target) then
return utilx.Clamp( current + increment, current, target )
elseif (current > target) then
return utilx.Clamp( current - increment, target, current )
end
return target
end
-- Prevention / Precision
-- Clamps the value between the minimum and maximum limits
function utilx.Clamp(value, min, max)
if (value < min) then
value = min
elseif (value > max) then
value = max
end
return value
end
-- Calculation
function utilx.Average(values)
total = 0
for i,v in pairs(values) do
total = total + values[i]
end
return (total / #values)
end
function utilx.StandardDeviation(values)
avg = 0
totaldev = 0
avg = utilx.Average(values);
for i,v in ipairs(values) do
totaldev = totaldev + math.pow(values[i] - avg, 2);
end
return math.sqrt(totaldev / #values);
end
-- Comparison
-- Determines whether the difference between the value and target is less than the permitted error
function utilx.Approximate(value, target, errorval)
return ( ( math.abs(value - target) < errorval) )
end
function utilx.CompareFloat(a, b, tolerance)
if ( ( a + tolerance ) < b ) then return -1 end
if ( ( b + tolerance ) < a ) then return 1 end
return 0
end
local pmeta = FindMetaTable("Player")
if pmeta then
// ########################
// FUNC: Player.GetUserGroup
// DESC: Get's the usergroup (i.e. admin/superadmin) of a player
// ARGS: none
// AUTH: BlackOps
function _R.Player:GetUserGroup()
return self:GetNetworkedString( "UserGroup" )
end
end
// ########################
// CONCMD: lua_openscript_sh
// DESC: Basically lua_openscript + lua_openscript_cl on a file at the same time
// ARGS: STRING path
// AUTH: FlapJack/Entoros
if CLIENT then
local function includeCallBack(um)
local path = um:ReadString()
if path then
include(path)
end
end
usermessage.Hook("LuaInclude" , includeCallBack)
else
local function includeCommand(pl , cmd , args)
if pl:IsSuperAdmin() then
umsg.Start("LuaInclude")
umsg.String(table.concat(" " , args))
umsg.End()
include(table.concat(" " , args))
end
end
local function GetPathBefore( path )
if not string.find(path,"/") then return "" end
local path = string.Explode( "/", path )
table.remove( path, #path )
return table.concat( path, "/" ) .. "/"
end
local function getAutoComplete(cmd,args)
local blocked = { ".", "..", }
local files = file.FindInLua( string.Trim(args) .. "*" )
for k,v in pairs( files ) do
local validfile = true
for blockedk,blockedv in pairs( blocked ) do
if string.find( blockedv, string.Trim(v) ) then
table.remove(files,k)
validfile = false
end
end
if validfile then
if file.IsDir( "../lua/"..GetPathBefore(args)..v ) then files[k] = cmd .." " .. GetPathBefore(args) .. v
else files[k] = cmd .. GetPathBefore(args) .. v end
end
end
return files
end
concommand.Add("lua_openscript_sh" , includeCommand,getAutoComplete)
end
// ########################
// FUNC: debug.getparams
// DESC: Gets the variable names of the parameters to a function
// ARGS: FUNCTION func
// AUTH: Deco
function debug.getparams(f)
local co = coroutine.create(f)
local params = {}
debug.sethook(co, function()
local i, k = 1, debug.getlocal(co, 2, 1)
while k do
if k ~= "(*temporary)" then
table.insert(params, k)
end
i = i+1
k = debug.getlocal(co, 2, i)
end
error("~~end~~")
end, "c")
local res, err = coroutine.resume(co)
if res then
error("The function provided defies the laws of the universe.", 2)
elseif string.sub(tostring(err), -7) ~= "~~end~~" then
error("The function failed with the error: "..tostring(err), 2)
end
return params
end
// ########################
// FUNC: chat.AddText
// DESC: Serverside func that allows you to add color text to chat
// ARGS: COLOR col OR PLAYER pl OR STRING text
// AUTH: Overv
if SERVER then
chat = { }
function chat.AddText( ... )
if ( type( arg[1] ) == "Player" ) then ply = arg[1] end
umsg.Start( "AddText", ply )
umsg.Short( #arg )
for _, v in pairs( arg ) do
if ( type( v ) == "string" ) then
umsg.String( v )
elseif ( type ( v ) == "table" ) then
umsg.Short( v.r )
umsg.Short( v.g )
umsg.Short( v.b )
umsg.Short( v.a )
end
end
umsg.End( )
end
else
usermessage.Hook( "AddText", function( um )
local argc = um:ReadShort( )
local args = { }
for i = 1, argc / 2, 1 do
table.insert( args, Color( um:ReadShort( ), um:ReadShort( ), um:ReadShort( ), um:ReadShort( ) ) )
table.insert( args, um:ReadString( ) )
end
chat.AddText( unpack( args ) )
end )
end
// ########################
// FUNC: draw.RoundedBoxOutlined
// DESC: Draw function that draws a runded box with an outline
// ARGS: NUMBER bordersize, NUMBER x, NUMBER y, NUMBER w, NUMBER h, COLOR bgcol, COLOR bordercol
// AUTH: Jinto?
if CLIENT then
function draw.RoundedBoxOutlined( bordersize, x, y, w, h, color, bordercol )
x = math.Round( x )
y = math.Round( y )
w = math.Round( w )
h = math.Round( h )
draw.RoundedBox( bordersize, x, y, w, h, color )
surface.SetDrawColor( bordercol )
surface.SetTexture( texOutlinedCorner )
surface.DrawTexturedRectRotated( x + bordersize/2 , y + bordersize/2, bordersize, bordersize, 0 )
surface.DrawTexturedRectRotated( x + w - bordersize/2 , y + bordersize/2, bordersize, bordersize, 270 )
surface.DrawTexturedRectRotated( x + w - bordersize/2 , y + h - bordersize/2, bordersize, bordersize, 180 )
surface.DrawTexturedRectRotated( x + bordersize/2 , y + h -bordersize/2, bordersize, bordersize, 90 )
surface.DrawLine( x+bordersize, y, x+w-bordersize, y )
surface.DrawLine( x+bordersize, y+h-1, x+w-bordersize, y+h-1 )
surface.DrawLine( x, y+bordersize, x, y+h-bordersize )
surface.DrawLine( x+w-1, y+bordersize, x+w-1, y+h-bordersize )
end
end
// ########################
// FUNC: Particle
// DESC: Like function "Sound", precaches particle system and returns the name
// ARGS: STRING particle_name
// AUTH: Entoros\
function Particle( name )
PrecacheParticleSystem( name )
return name
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment