Skip to content

Instantly share code, notes, and snippets.

@Kefta
Last active November 11, 2018 00:20
Show Gist options
  • Save Kefta/6bf977639c1e04206b5d5d1e14d6e4d6 to your computer and use it in GitHub Desktop.
Save Kefta/6bf977639c1e04206b5d5d1e14d6e4d6 to your computer and use it in GitHub Desktop.
Rainbow mode for Robotboy655's lightsabers. Works but the code is still pretty atrocious
-- gs_common by code_gs
-- Last updated 11/10/2018
-- Goes in lua/gs_common.lua
-- gs_lib on the way, but for now, here's a common file to dump everything for misc scripts
AddCSLuaFile()
-- Localise globals to save on global lookups in type checking functions
-- Need to eek out as much performance as possible to have a minimal footprint on runtime
local Msg = Msg
local type = type
local NULL = NULL
local error = error
local pairs = pairs
local Color = Color
local TypeID = TypeID
local CLIENT = CLIENT
local SERVER = SERVER
local istable = istable
local isnumber = isnumber
local tonumber = tonumber
local tostring = tostring
local math_log = math.log
local math_max = math.max
local math_min = math.min
local GetConVar = GetConVar
local TYPE_TABLE = TYPE_TABLE
local math_floor = math.floor
local ErrorNoHalt = ErrorNoHalt
local TYPE_ENTITY = TYPE_ENTITY
local TYPE_NUMBER = TYPE_NUMBER
local TYPE_STRING = TYPE_STRING
local string_lower = string.lower
local table_insert = table.insert
local table_remove = table.remove
local TYPE_USERDATA = TYPE_USERDATA
local debug_getinfo = debug.getinfo
local string_format = string.format
local TYPE_LIGHTUSERDATA = TYPE_LIGHTUSERDATA
local gs = {}
local gs_debug = CreateConVar("gs_debug", "0", FCVAR_ARCHIVE, "Enable debug output for gs addons")
function gs.DevMsg(iLevel, sMsg)
if (gs_debug:GetInt() >= iLevel) then
Msg(string_format("[GS] %s\n", sMsg))
end
end
-- Stole this type-system code from an upcoming PR I'm working on
-- which is why it uses a different style from the rest of the file
-- Ew.. need a way to convert type IDs to names
local LuaTypes = {
[ TYPE_NONE ] = type(),
[ TYPE_NIL ] = type( nil ),
[ TYPE_BOOL ] = type( false ),
[ TYPE_LIGHTUSERDATA ] = type( newproxy() ),
[ TYPE_NUMBER ] = type( 0 ),
[ TYPE_STRING ] = type( "" ),
[ TYPE_TABLE ] = type( {} ),
[ TYPE_FUNCTION ] = type( function() end ),
[ TYPE_USERDATA ] = type( newproxy() ),
[ TYPE_THREAD ] = type( coroutine.create( function() end ) )
}
local ArgErr = "bad argument #%d"
local ArgErrFunc = "bad argument #%d to '%s'"
local ArgsErr = "bad arguments %s"
local ArgsErrFunc = "bad arguments %s to '%s'"
local TypeErr = "%s expected, got %s"
local TypeErrIndex = "%s expected at index %d, got %s"
local TypeErrValid = "valid %s expected, got %s"
local BadSelfErr = "calling '%s' on bad self"
local AnyErr = "value expected"
local registry = debug.getregistry()
local entity = FindMetaTable( "Entity" ) or {}
local function fTypeName( id, safe --[[= true]] )
local type = LuaTypes[ id ]
if ( type ) then return type end
-- HACK: Entity has multiple metatables have the same MetaID (Weapon, NPC, Player, etc.)
if ( id == TYPE_ENTITY ) then
return entity.MetaName || LuaTypes[ TYPE_USERDATA ]
end
-- Scan the registry for the type's name
-- Fallback to "UserData" just like type() does
for k, v in pairs( registry ) do
if ( istable( v ) && v.MetaID == id ) then
return tostring( v.MetaName ) || LuaTypes[ TYPE_USERDATA ]
end
end
-- Always return a string to be inline safe
if ( safe == nil || safe ) then
return string_format( "(type %s)", id )
end
return nil
end
-- Foward-declare for TypeName
local fCheckNumber
local function fTypeName( id, safe --[[= true]] )
id = fCheckNumber( id )
return fTypeName( id, safe )
end
gs.TypeName = fTypeName
local function CommaList( tbl, len, formatFunc, sep, ... )
local list = ""
-- Two arguments: "#1 and #2"
if ( len == 2 ) then
list = formatFunc( arg[ 1 ] )
-- Three+ arguments: "#1, #2, ..., and #len"
else
for i = 1, len - 1 do
list = list .. formatFunc( arg[ i ], i, ... ) .. ", "
end
end
return string_format( "%s %s %s ", list, sep, formatFunc( arg[ len ], i, ... ) )
end
-- Foward-declare for FormArgument
local fArgError
local function FormArgument( arg, i, method, level )
local narg = tonumber( arg )
if ( !narg ) then
local err
if ( i == nil ) then
err = string_format( TypeErr, fTypeName( TYPE_NUMBER ), type( arg ) )
else
err = string_format( TypeErrIndex, fTypeName( TYPE_NUMBER ), i, type( arg ) )
end
fArgError( 1, err, true, level )
end
if ( method ) then
if ( narg == 1 ) then return "self" end
return string_format( "#%d", narg - 1 )
end
return string_format( "#%d", narg )
end
local function FormArgError( arg, info, self, multi )
if ( self && info.name ) then
return string_format( BadSelfErr, info.name )
end
if ( info && info.name ) then
return string_format( multi && ArgsErrFunc || ArgErrFunc, arg, info.name )
end
return string_format( multi && ArgsErr || ArgErr, arg, info.name )
end
fArgError = function( arg, err, halt --[[= true]], level --[[= 1]] )
if ( level == nil ) then level = 1 end
level = level + 1
local info = debug_getinfo( level, "n" )
local method = info && info.namewhat == "method"
local prefix
if ( istable( arg ) ) then
local len = #arg
if ( len == 0 ) then
fArgError( 1, "expected at least one table element", true, 1 )
return
end
if ( len == 1 ) then
local arg, self = FormArgument( arg[ 1 ], 1, method, 2 )
prefix = FormArgError( arg, info, self, false )
else
prefix = FormArgError( CommaList( arg, list, FormArgument, "and", method, 3 ), info, self, true )
end
else
local arg, self = FormArgument( arg, nil, method, 2 )
prefix = FormArgError( arg, info, self, false )
end
err = string_format( "%s (%s)", prefix, err )
if ( halt || halt == nil ) then
error( err, level + 1 )
else
ErrorNoHalt( err )
end
end
gs.ArgError = fArgError
function gs.IsType( val, Type )
if ( istable( Type ) ) then
local checktype = TypeID( val )
for i = 1, #Type do
if ( checktype == Type[ i ] ) then return true end
end
return false
end
return TypeID( val ) == Type
end
local function TypeNameCheck( id, i, level )
if ( TypeID( id ) == TYPE_NUMBER ) then
-- https://github.com/lua/lua/blob/f59e6a93c0ad38a27a420e51abf8f13d962446b5/lauxlib.c#L188
if ( id == TYPE_LIGHTUSERDATA ) then
return "light userdata"
end
return fTypeName( id )
end
local err
if ( i == nil ) then
err = string_format( TypeErr, fTypeName( TYPE_NUMBER ), type( id ) )
else
err = string_format( TypeErrIndex, fTypeName( TYPE_NUMBER ), i, type( id ) )
end
fArgError( 3, err, true, level )
end
local function fTypeError( arg, sError, halt, level )
local argtype = TypeID( arg )
if ( argtype != TYPE_NUMBER && argtype != TYPE_TABLE ) then
fArgError( 2, string_format( TypeErr, fTypeName( TYPE_NUMBER ) .. " or " .. fTypeName( TYPE_TABLE ), type( arg ) ), true, 2 )
end
if ( level == nil ) then level = 2 else level = level + 1 end
fArgError( arg, string_format( TypeErr, expected, type( val ) ), halt, level )
end
function gs_CheckType( val, arg, Type, halt --[[= true]], level --[[= 1]] )
local expected, rettype
-- Check for a table of types first
if ( istable( Type ) ) then
local len = #Type
rettype = TypeID( val )
for i = 1, len do
local Type = Type[ i ]
if ( rettype == Type ) then return Type end
end
if ( len == 0 ) then
fArgError( 3, "expected at least one table element", true, 1 )
end
if ( len == 1 ) then
expected = TypeNameCheck( Type, 1, 2 )
else
expected = CommaList( Type, len, TypeNameCheck, "or", 3 )
end
else
rettype = TypeID( val )
-- Do a blind check against the TypeID next
if ( rettype == Type ) then
return Type
end
-- Now that we've confirmed an error is going to be thrown,
-- take the time to check the type of the Type arg
expected = TypeNameCheck( Type, nil, 2 )
end
fTypeError( arg, string_format( TypeErr, expected, type( val ) ), halt, level )
return rettype
end
gs.CheckType = gs_CheckType
function gs.CheckNumber( val, arg, halt --[[= true]], level --[[= 1]] )
local num = tonumber( val )
if ( num ~= nil ) then
return num
end
fTypeError( arg, string_format( TypeErr, fTypeName( TYPE_NUMBER ), type( val ) ), halt, level )
end
function gs.CheckString( val, arg, halt --[[= true]], level --[[= 1]] )
local id = TypeID( val )
if ( id == TYPE_NUMBER ) then
return tostring( val )
end
if ( id == TYPE_STRING ) then
return val
end
fTypeError( arg, string_format( TypeErr, fTypeName( TYPE_STRING ), type( val ) ), halt, level )
end
function gs.CheckUserData( val, arg, halt --[[= true]], level --[[= 1]] )
local id = TypeID( val )
if ( id == TYPE_USERDATA || id == TYPE_LIGHTUSERDATA ) then
return val
end
fTypeError( arg, string_format( TypeErr, fTypeName( TYPE_USERDATA ), type( val ) ), halt, level )
end
function gs.CheckAny( val, arg, halt --[[= true]], level --[[= 1]] )
if ( val ~= nil ) then
return val
end
fTypeError( arg, AnyErr, halt, level )
end
gs_CheckAny = gs.CheckAny
function gs.CheckEntity( val, arg, halt --[[= true]], level --[[= 1]] )
if ( TypeID( val ) == TYPE_ENTITY ) then
return val
end
fTypeError( arg, string_format( TypeErr, fTypeName( TYPE_ENTITY ), type( val ) ), halt, level )
end
function gs.CheckValidEntity( val, arg, halt --[[= true]], level --[[= 1]] )
if ( TypeID( val ) == TYPE_ENTITY ) then
if ( val:IsValid() ) then
return val
end
fTypeError( arg, string_format( TypeErrValid, fTypeName( TYPE_ENTITY ), type( val ) ), halt, level )
else
fTypeError( arg, string_format( TypeErr, fTypeName( TYPE_ENTITY ), type( val ) ), halt, level )
end
end
local function EntityFunctionBuilder( name, func, valid )
gs[ "Check" .. name ] = function( val, arg, halt --[[= true]], level --[[= 1]] )
if ( TypeID( val ) == TYPE_ENTITY ) then
if ( val == NULL || val[func]() ) then
return val
end
end
fTypeError( arg, string_format( TypeErr, name, type( val ) ), halt, level )
end
if ( valid ) then
gs[ "CheckValid" .. name ] = function( val, arg, halt --[[= true]], level --[[= 1]] )
if ( TypeID( val ) == TYPE_ENTITY ) then
if ( val[func]() ) then
return val
end
end
fTypeError( arg, string_format( TypeErr, name, type( val ) ), halt, level )
end
end
end
EntityFunctionBuilder( "NPC", "IsNPC", true )
EntityFunctionBuilder( "Player", "IsPlayer", true )
EntityFunctionBuilder( "Weapon", "IsWeapon", true )
EntityFunctionBuilder( "Vehicle", "IsVehicle", false )
local gs_CheckValidPlayer = gs.CheckValidPlayer
-- Vehicles have two validity functions
function gs.CheckValidVehicle( val, arg, halt --[[= true]], level --[[= 1]] )
if ( TypeID( val ) == TYPE_ENTITY ) then
if ( val:IsVehicle() and val:IsValidVehicle() ) then
return val
end
end
fTypeError( arg, string_format( TypeErr, "Vehicle", type( val ) ), halt, level )
end
-- End Lua func PR
function gs.Clamp(dNum, dMin, dMax)
return math_min(math_max(dNum, dMin), dMax)
end
-- The minimum number of bits needed to network a number
-- Local versions of functions don't have type checking
local function gs_BitCount(uiNum)
return math_floor(math_log(uiNum, 2)) + 1
end
function gs.BitCount(uiNum)
-- FIXME: Add CheckInteger, CheckUnsignedInteger
gs_CheckType(uiNum, 1, TYPE_NUMBER)
return math_floor(math_log(math_floor(uiNum), 2)) + 1
end
-- Credit to Nak: https://github.com/Facepunch/garrysmod/pull/1202
-- HSL's third argument is lightness instead of value
-- But both are 0-1 so no input changes are needed
local function gs_HSLToColor(H, S, L)
local C = (1-math.abs(2*L-1)) * S
local X = C*(1-math.abs((H/60)%2-1))
local m = L-C/2
local R1,G1,B1 = 0,0,0
if H < 60 then R1,G1,B1 = C,X,0
elseif H < 120 then R1,G1,B1 = X,C,0
elseif H < 180 then R1,G1,B1 = 0,C,X
elseif H < 240 then R1,G1,B1 = 0,X,C
elseif H < 300 then R1,G1,B1 = X,0,C
else --[[ H<360 ]] R1,G1,B1 = C,0,X
end
return Color((R1+m)*255,(G1+m)*255,(B1+m)*255)
end
function gs.HSLToColor(dHue, dSaturation, dLightness)
-- FIXME: Add CheckRange
gs_CheckType(dHue, 1, TYPE_NUMBER)
gs_CheckType(dSaturation, 2, TYPE_NUMBER)
gs_CheckType(dLightness, 3, TYPE_NUMBER)
return gs_HSLToColor(dHue % 360, dSaturation % 360, dLightness % 360)
end
-- FIXME: https://github.com/Facepunch/garrysmod-issues/issues/2407
function gs_IsColor(tbl)
return istable(tbl) and isnumber(tbl.r) and isnumber(tbl.g) and isnumber(tbl.b) and isnumber(tbl.a)
end
gs.IsColor = gs_IsColor
gs.MAX_BITS_INTEGER = 32
gs.MAX_BITS_PLAYER = gs_BitCount(game.MaxPlayers(), 2)
gs.MAX_BITS_CONVAR = 0
local bMultiPlayer = not game.SinglePlayer()
local gs_ReadPlayerIndex, gs_WritePlayerIndex
if (bMultiPlayer) then
function gs.WritePlayer(pPlayer)
gs_CheckType(pPlayer, 1, TYPE_ENTITY)
if (not pPlayer:IsPlayer()) then
fArgError(1, "Player expected")
end
net.WriteUInt(pPlayer:EntIndex() - 1, gs.MAX_BITS_PLAYER)
end
function gs.ReadPlayer()
return Entity(net.ReadUInt(gs.MAX_BITS_PLAYER) + 1)
end
local gs_WritePlayerIndex = function(uiIndex)
net.WriteUInt(uiIndex - 1, gs.MAX_BITS_PLAYER)
end
function gs.WritePlayerIndex(uiIndex)
gs_CheckType(uiIndex, 1, TYPE_NUMBER)
net.WriteUInt(uiIndex - 1, gs.MAX_BITS_PLAYER)
end
local gs_ReadPlayerIndex = function()
return net.ReadUInt(gs.MAX_BITS_PLAYER) + 1
end
gs.ReadPlayerIndex = gs_ReadPlayerIndex
else
function gs.WritePlayer()
end
function gs.ReadPlayer()
return Entity(1)
end
local gs_WritePlayerIndex = function()
end
gs.WritePlayerIndex = gs_WritePlayerIndex
local gs_ReadPlayerIndex = function()
return 1
end
gs.ReadPlayerIndex = gs_ReadPlayerIndex
end
local tConVarList = {}
local tConVarWrite = {}
local tConVarValues = {}
local tConVarValuesLen = {}
local tConVarDefault = {}
local uiConVarCount = 0
local uiConVarPlayers = 0 -- FIXME: Bound these values by 32-bit int
if (bMultiPlayer) then
function gs.GetPlayerSetting(pPlayer, sName, Default --[[= tConVarDefault[sName]])
gs_CheckValidPlayer(pPlayer, 1)
gs_CheckType(sName, 2, TYPE_STRING)
local Val = tConVarValues[pPlayer:EntIndex()]
if (Val ~= nil) then
Val = Val[sName]
if (Val ~= nil) then
return Val
end
end
if (Default == nil) then
Val = tConVarDefault[sName]
if (Val ~= nil) then
return Val
end
end
return Default
end
else
function gs.GetPlayerSetting(_, sName, Default --[[= tConVarDefault[sName]])
gs_CheckType(sName, 2, TYPE_STRING)
local Val = tConVarValues[sName]
if (Val ~= nil) then
return Val
end
if (Default == nil) then
Val = tConVarDefault[sName]
if (Val ~= nil) then
return Val
end
end
return Default
end
end
local fPassThrough = function(val)
return val
end
local tTypeTables = {
[TYPE_BOOL] = {
Read = net.ReadBool,
Write = net.WriteBool,
InBounds = function(bool)
return true, bool
end,
ToString = function(bool)
if (bool) then
return "1"
end
return "0"
end,
FromString = function(sBool)
if (sBool == "0") then
return false
end
return true
end
},
[TYPE_NUMBER] = {
Read = net.ReadDouble,
Write = net.WriteDouble,
InBounds = function(num)
return true, num
end,
ToString = tostring,
FromString = tonumber
},
[TYPE_STRING] = {
Read = net.ReadString,
Write = net.WriteString,
InBounds = function(str)
return true, str
end,
ToString = fPassThrough,
FromString = fPassThrough
}
}
-- FIXME: Add CheckField
local function CheckFuncTableKey(tFuncs, sKey)
if (TypeID(tFuncs[sKey]) ~= TYPE_FUNCTION) then
fArgError(3, string_format(TypeErrIndex, fTypeName(TYPE_FUNCTION), type(tFuncs[sKEy])), 3)
end
end
local tFuncArgs = {TYPE_NUMBER, TYPE_TABLE}
local function CheckFuncTable(tFuncs)
if (gs_CheckType(tFuncs, 3, tFuncArgs) == TYPE_NUMBER) then
tFuncs = tTypeTables[tFuncs]
if (tFuncs == nil) then
fArgError(3, "type not supported", 2)
end
return tFuncs
end
CheckFuncTableKey(tFuncs, "Read")
CheckFuncTableKey(tFuncs, "Write")
CheckFuncTableKey(tFuncs, "InBounds")
CheckFuncTableKey(tFuncs, "ToString")
CheckFuncTableKey(tFuncs, "FromString")
return tFuncs
end
local sConvarErr = "CONVAR MISMATCH [%s]: %s (%s) sent invalid value \"%s\" to the server (%s)"
local sTypeSettingErr = "type not supported"
if (SERVER) then
util.AddNetworkString("gs_convar_add")
util.AddNetworkString("gs_convar_sync")
if (bMultiPlayer) then
local tSyncedPlayers = {}
local uiSyncedPlayers = 0
local tSyncedPlayerLookup = {}
-- FIXME: Do something about default values and casting from string->desired convar type
function gs.PlayerSetting(sName, Default, tFuncs)
gs_CheckType(sName, 1, TYPE_STRING)
sName = string_lower(sName)
if (tConVarList[sName] == nil) then
Default = gs_CheckAny(Default, 2)
tFuncs = CheckFuncTable(tFuncs)
-- FIXME: Check if these are non-nil
local fRead = tFuncs.Read
local fWrite = tFuncs.Write
local fBounds = tFuncs.InBounds
local fToString = tFuncs.ToString
local uiConVarID = uiConVarCount + 1
uiConVarCount = uiConVarID
gs.MAX_BITS_CONVAR = gs_BitCount(uiConVarID)
tConVarList[uiConVarID] = sName
tConVarList[sName] = uiConVarID
tConVarWrite[uiConVarID] = fWrite
tConVarDefault[sName] = Default
local sNetName = "gs_convar_sync_" .. sName
util.AddNetworkString(sNetName)
net.Receive(sNetName, function(_, pPlayer)
-- FIXME: Add ConVarToString function for this
local ValClient = fRead()
local bInBounds, Val = fBounds(ValClient)
if (not bInBounds) then
gs.DevMsg(2, string.format("CONVAR MISMATCH [%s]: %s (%s) sent invalid value \"%s\" to the server (%s)", sName, pPlayer:Nick(), pPlayer:SteamID(), ValClient, Val))
return
end
local uiPlayer = pPlayer:EntIndex()
local tValues = tConVarValues[uiPlayer]
if (tValues == nil) then
uiConVarPlayers = uiConVarPlayers + 1
tValues = {[0] = 1}
tConVarValues[uiPlayer] = tValues
elseif (tValues[uiConVarID] == nil) then
tValues[0] = tValues[0] + 1
end
tValues[uiConVarID] = Val
-- Need to temporarily omit the player who sent this update from the receivers table
local uiIndex = tSyncedPlayerLookup[pPlayer:EntIndex()]
if (uiIndex ~= nil) then
table_remove(tSyncedPlayers, uiIndex)
end
net.Start(sNetName)
gs.WritePlayerIndex(uiPlayer)
fWrite(Val)
net.Send(tSyncedPlayers)
if (uiIndex ~= nil) then
table_insert(tSyncedPlayers, uiIndex, pPlayer)
end
end)
net.Start("gs_convar_add")
net.WriteString(sName)
net.Send(tSyncedPlayers)
end
end
net.Receive("gs_convar_sync", function(_, pPlayer)
-- Network other client's info to the new player
net.Start("gs_convar_sync")
net.WriteUInt(uiConVarCount, gs.MAX_BITS_INTEGER)
for i = 1, uiConVarCount do
net.WriteString(tConVarList[i])
end
-- Player count
net.WriteUInt(uiConVarPlayers, gs.MAX_BITS_INTEGER)
-- FIXME: Used sized array and ranged loop instead?
for uiPlayer, tValues in pairs(tConVarValues) do
local uiConVars = tValues[0]
net.WriteUInt(uiConVars, gs.MAX_BITS_INTEGER)
gs.WritePlayerIndex(uiPlayer)
for uiConVar, Val in pairs(tValues) do
if (uiConVar ~= 0) then
net.WriteUInt(uiConVar, gs.MAX_BITS_CONVAR)
if (tConVarWrite[uiConVar] == nil) then
error("Error! " .. uiConVar .. " didn't have a write function")
continue
end
tConVarWrite[uiConVar](Val)
end
end
end
net.Send(pPlayer)
uiSyncedPlayers = uiSyncedPlayers + 1
tSyncedPlayers[uiSyncedPlayers] = pPlayer
tSyncedPlayerLookup[pPlayer:EntIndex()] = uiSyncedPlayers
end)
gameevent.Listen("player_disconnect")
hook.Add("EntityRemoved", "gs_convar", function(pEntity)
local uiID = pEntity:EntIndex()
local uiIndex = tSyncedPlayerLookup[uiID]
if (uiIndex ~= nil) then
tSyncedPlayers[uiIndex] = nil
tSyncedPlayerLookup[uiID] = nil
end
end)
else
function gs.PlayerSetting(sName, Default, tFuncs)
gs_CheckType(sName, 1, TYPE_STRING)
sName = string_lower(sName)
if (tConVarList[sName] == nil) then
Default = gs_CheckAny(Default, 2)
CheckFuncTable(tFuncs)
-- FIXME: Check if these are non-nil
local fRead = tFuncs.Read
local fWrite = tFuncs.Write
local fBounds = tFuncs.InBounds
local fToString = tFuncs.ToString
local uiConVarID = uiConVarCount + 1
uiConVarCount = uiConVarID
gs.MAX_BITS_CONVAR = gs_BitCount(uiConVarID)
tConVarList[uiConVarID] = sName
tConVarList[sName] = uiConVarID
tConVarWrite[uiConVarID] = fWrite
tConVarDefault[sName] = Default
local sNetName = "gs_convar_sync_" .. sName
util.AddNetworkString(sNetName)
net.Receive(sNetName, function(_, pPlayer)
-- FIXME: Add ConVarToString function for this
local ValClient = fRead()
local bInBounds, Val = fBounds(ValClient)
if (not bInBounds) then
gs.DevMsg(2, string.format(sConvarErr, sName, pPlayer:Nick(), pPlayer:SteamID(), ValClient, Val))
return
end
local uiPlayer = pPlayer:EntIndex()
local tValues = tConVarValues[uiPlayer]
if (tValues == nil) then
uiConVarPlayers = uiConVarPlayers + 1
tValues = {[0] = 1}
tConVarValues[uiPlayer] = tValues
elseif (tValues[uiConVarID] == nil) then
tValues[0] = tValues[0] + 1
end
tValues[uiConVarID] = Val
net.Start(sNetName)
fWrite(Val)
net.Broadcast()
end)
net.Start("gs_convar_add")
net.WriteString(sName)
net.Broadcast()
end
end
end
else
local tRecursion = {}
if (bMultiPlayer) then
local tConVarRead = {}
local tConVarClamp = {}
local tConVarToString = {}
local tConVarFromString = {}
local tConVarValuesLocal = {}
function gs.PlayerSetting(sName, Default, tFuncs, uiFlags --[[= FCVAR_NONE]], sHelp --[[= ""]])
sName = string_lower(sName)
if (tConVarDefault[sName] == nil) then
if (isnumber(tFuncs)) then
tFuncs = tTypeTables[tFuncs]
if (tFuncs == nil) then
fArgError(3, "type not supported")
end
end
tConVarRead[sName] = tFuncs.Read
tConVarWrite[sName] = tFuncs.Write
tConVarClamp[sName] = tFuncs.InBounds
local fToString = tFuncs.ToString
tConVarToString[sName] = fToString
tConVarFromString[sName] = tFuncs.FromString
tConVarDefault[sName] = Default
return CreateConVar(sName, fToString(Default), uiFlags, sHelp)
end
end
hook.Add("InitPostEntity", "gs_convar", function()
local pPlayer = LocalPlayer()
if (pPlayer:IsValid()) then
tConVarValues[pPlayer:EntIndex()] = tConVarValuesLocal
else
gs.DevMsg(1, "failed to initialise convar system (LocalPlayer isn't valid)")
end
-- Entities are done loading, the client is ready to talk with the server
net.Start("gs_convar_sync")
net.SendToServer()
end)
local function AddConVar(sConVar, uiConvar)
-- FIXME: Allow resyncs
if (tConVarList[sConVar] ~= nil) then
return
end
local Default = tConVarDefault[sConVar]
if (Default == nil) then
gs.DevMsg(1, string_format("unregistered convar \"%s\" received from the server", sConVar))
return
end
tConVarList[uiConvar] = sConVar
tConVarList[sConVar] = uiConvar
local fRead = tConVarRead[sConVar]
local fWrite = tConVarWrite[sConVar]
local fBounds = tConVarClamp[sConVar]
local fToString = tConVarToString[sConVar]
local fFromString = tConVarFromString[sConVar]
local sDefault = fToString(Default)
-- Now that these are binded to the functions that need them, we can remove them from the waiting tables
tConVarWrite[sConVar] = nil
tConVarClamp[sConVar] = nil
tConVarToString[sConVar] = nil
tConVarFromString[sConVar] = nil
local BoundConVar = function(sName, sValPrev, sValNew)
if (tRecursion[sName] == nil) then
local ValNew = fFromString(sValNew)
local bInBounds, Val
if (ValNew == nil) then
bInBounds = false
Val = "couldn't convert value"
else
bInBounds, Val = fBounds(ValNew)
end
if (not bInBounds) then
if (fBounds(fFromString(sValPrev))) then
local ValSend = fFromString(sValPrev)
tConVarValuesLocal[sName] = ValSend
net.Start("gs_convar_sync_" .. sName)
fWrite(ValSend)
net.SendToServer()
-- Old value is bad, use the default
else
tConVarValuesLocal[sName] = nil
sValPrev = sDefault
end
Msg(string_format("bad value to convar \"%s\" (%s)\n", sName, Val))
tRecursion[sName] = true
GetConVar(sName):SetString(sValPrev)
tRecursion[sName] = nil
else
tConVarValuesLocal[sName] = Val
local sValClamped = fToString(Val)
-- Was the value clamped?
if (sValNew ~= sValClamped) then
tRecursion[sName] = true
GetConVar(sName):SetString(sValClamped)
tRecursion[sName] = nil
end
net.Start("gs_convar_sync_" .. sName)
fWrite(Val)
net.SendToServer()
end
end
end
-- HACK: Bound FCVAR_ARCHIVE presets
local convar = GetConVar(sConVar)
local sCurVal = convar:GetString()
local sNetName = "gs_convar_sync_" .. sConVar
BoundConVar(sConVar, sCurVal, sCurVal)
cvars.AddChangeCallback(sConVar, BoundConVar)
net.Receive(sNetName, function()
local uiPlayer = gs.ReadPlayerIndex()
local tValues = tConVarValues[uiPlayer]
if (tValues == nil) then
uiConVarPlayers = uiConVarPlayers + 1
tValues = {[0] = 1}
tConVarValues[uiPlayer] = tValues
elseif (tValues[sConVar] == nil) then
tValues[0] = tValues[0] + 1
end
tValues[sConVar] = fRead()
end)
gs.DevMsg(1, string_format("convar \"%s\" received from the server", sConVar))
end
net.Receive("gs_convar_add", function()
uiConVarCount = uiConVarCount + 1
gs.MAX_BITS_CONVAR = gs_BitCount(uiConVarCount)
AddConVar(net.ReadString(), uiConVarCount)
end)
net.Receive("gs_convar_sync", function()
uiConVarCount = net.ReadUInt(gs.MAX_BITS_INTEGER)
gs.MAX_BITS_CONVAR = gs_BitCount(uiConVarCount)
gs.DevMsg(2, string_format("starting convar sync with %u convars", uiConVarCount))
-- HACK TO THE MAX: was getting a net misalignment here when reading and adding at the same time for some odd reason: https://i.imgur.com/SjvpAih.png
-- Net overrides are showing nothing else being called, still not sure what is cauing it
local tbl = {}
for i = 1, uiConVarCount do
tbl[i] = net.ReadString()
end
for i = 1, uiConVarCount do
AddConVar(tbl[i], i)
end
uiConVarPlayers = net.ReadUInt(gs.MAX_BITS_INTEGER)
for i = 1, uiConVarPlayers do
local uiConVars = net.ReadUInt(gs.MAX_BITS_INTEGER)
local tValues = {[0] = uiConVars}
tConVarValues[gs.ReadPlayerIndex()] = tValues
for i = 1, uiConVars do
local sConVar = tConVarList[net.ReadUInt(gs.MAX_BITS_CONVAR)]
local Val = tConVarRead[sConVar]()
tValues[sConVar] = Val
end
end
end)
hook.Add("EntityRemoved", "gs_convar", function(pEntity)
tConVarValues[pEntity:EntIndex()] = nil
end)
else
function gs.PlayerSetting(sName, Default, tFuncs, uiFlags --[[= FCVAR_NONE]], sHelp --[[= ""]])
sName = string_lower(sName)
if (tConVarDefault[sName] == nil) then
-- Retrieve the type table for this type
if (isnumber(tFuncs)) then
tFuncs = tTypeTables[tFuncs]
if (tFuncs == nil) then
fArgError(3, "type not supported")
end
end
-- FIXME: Check if these are non-nil
local fBounds = tFuncs.InBounds
local fToString = tFuncs.ToString
cvars.AddChangeCallback(sName, function(sName, sValPrev, sValNew)
if (tRecursion[sName] == nil) then
local ValNew = fFromString(sValNew)
local bInBounds, Val
if (ValNew == nil) then
bInBounds = false
Val = "couldn't convert value"
else
bInBounds, Val = fBounds(ValNew)
end
if (not bInBounds) then
-- Old value is bad, use the default
if (not fBounds(fFromString(sValPrev))) then
sValPrev = sDefault
end
Msg(string_format("bad value to convar \"%s\" (%s)", sName, Val))
tRecursion[sName] = true
GetConVar(sName):SetString(sValPrev)
tRecursion[sName] = nil
else
tConVarValues[sName] = Val
local sValClamped = fToString(Val)
if (sValNew ~= sValClamped) then
tRecursion[sName] = true
GetConVar(sName):SetString(sValClamped)
tRecursion[sName] = nil
end
end
end
end)
tConVarDefault[sName] = Default
return CreateConVar(sName, fToString(Default), uiFlags, sHelp)
end
end
end
end
return gs
-- gs_rb655_lightsaber_rainbow
-- Last updated 11/10/2018
-- Goes in lua/autorun/gs_rb655_lightsaber_rainbow.lua
-- Requires Robotboy655's Lightsabers: https://steamcommunity.com/sharedfiles/filedetails/?id=111412589
-- Rainbow options are in Tools>Robotboy655>Lightsaber at the bottom
local gs = include("gs_common.lua")
local tUnitFuncs = {
Read = net.ReadDouble,
Write = net.WriteDouble,
InBounds = function(num)
if (num >= 0 and num <= 1) then
return true, num
end
return false, "expected number [0, 1]"
end,
ToString = tostring,
FromString = tonumber
}
local tFrequencyFuncs = {
Read = net.ReadDouble,
Write = net.WriteDouble,
InBounds = function(num)
if (num >= 0 and num <= 120) then
return true, num
end
return false, "expected number [0, 120]"
end,
ToString = tostring,
FromString = tonumber
}
-- FIXME: Check convar values when the player joins
gs.PlayerSetting("gs_rb655_lightsaber_rainbow", false, TYPE_BOOL, FCVAR_ARCHIVE, "Enable rainbow mode for lightsabers")
gs.PlayerSetting("gs_rb655_lightsaber_rainbow_hsl", false, TYPE_BOOL, FCVAR_ARCHIVE, "Use HSL for colour interpolation instead of HSV")
gs.PlayerSetting("gs_rb655_lightsaber_rainbow_frequency", 60, tFrequencyFuncs, FCVAR_ARCHIVE, "Frequency of colour changing")
gs.PlayerSetting("gs_rb655_lightsaber_rainbow_saturation", 1, tUnitFuncs, FCVAR_ARCHIVE, "Saturation of the lightsaber's colour")
gs.PlayerSetting("gs_rb655_lightsaber_rainbow_value", 1, tUnitFuncs, FCVAR_ARCHIVE, "Value in HSV mode and lightness in HSL mode")
if (CLIENT) then
-- STools are registered by now as opposed to AddToolMenuTabs/Categories
hook.Add("PopulateToolMenu", "gs_rb655_lightsaber_rainbow", function()
local tToolTabs = spawnmenu.GetToolMenu("Main")
-- Try to add options to the default lightsaber menu first
for i = 1, #tToolTabs do
local tMenu = tToolTabs[i]
if (tMenu.ItemName == "Robotboy655") then
for i = 1, #tMenu do
local tCategory = tMenu[i]
if (tCategory.ItemName == "rb655_lightsaber") then
local fOldFunc = tCategory.CPanelFunction
tCategory.CPanelFunction = function(pPanel)
if (fOldFunc ~= nil) then
fOldFunc(pPanel)
end
pPanel:AddControl("CheckBox", {Label = "Rainbow mode", Command = "gs_rb655_lightsaber_rainbow"})
pPanel:AddControl("CheckBox", {Label = "Rainbow HSL mode", Command = "gs_rb655_lightsaber_rainbow_hsl"})
pPanel:AddControl("Slider", {Label = "Rainbow frequency", Command = "gs_rb655_lightsaber_rainbow_frequency", Type = "Float", Min = "0", Max = "120"})
pPanel:AddControl("Slider", {Label = "Rainbow saturation", Command = "gs_rb655_lightsaber_rainbow_saturation", Type = "Float", Min = "0", Max = "1"})
pPanel:AddControl("Slider", {Label = "Rainbow value", Command = "gs_rb655_lightsaber_rainbow_value", Type = "Float", Min = "0", Max = "1"})
end
end
end
end
end
-- If that fails, add our own settings
-- NOTE: Commented out since there's no way to tell if the lightsaber addon
-- is even installed by the time the spawnmenu is populated. I've opted to
-- instead create nothing if the default menu can't be found,
-- but uncommenting this code will create one anyway under Util>User>Lightsabers
--[[spawnmenu.AddToolMenuOption("Main", "User", "gs_rb655_lightsaber_rainbow", "Lightsabers", "", "", function(pPanel)
pPanel:AddControl("CheckBox", {Label = "Rainbow mode", Command = "gs_rb655_lightsaber_rainbow"})
pPanel:AddControl("CheckBox", {Label = "Rainbow HSL mode", Command = "gs_rb655_lightsaber_rainbow_hsl"})
pPanel:AddControl("Slider", {Label = "Rainbow frequency", Command = "gs_rb655_lightsaber_rainbow_frequency", Type = "Float", Min = "0", Max = "120"})
pPanel:AddControl("Slider", {Label = "Rainbow saturation", Command = "gs_rb655_lightsaber_rainbow_saturation", Type = "Float", Min = "0", Max = "1"})
pPanel:AddControl("Slider", {Label = "Rainbow value", Command = "gs_rb655_lightsaber_rainbow_value", Type = "Float", Min = "0", Max = "1"})
end)]]
end)
hook.Add("gs_rb655_lightsaber_GetColor", "gs_rb655_lightsaber_rainbow", function(self)
local pPlayer = self:GetOwner()
if (pPlayer:IsPlayer() and not pPlayer:IsBot() and gs.GetPlayerSetting(pPlayer, "gs_rb655_lightsaber_rainbow")) then
-- Setting lookups aren't needed in single-player - use a simple convar lookup instead
-- Use tonumber(var:GetString()) instead of GetFloat for more precision
return (gs.GetPlayerSetting(pPlayer, "gs_rb655_lightsaber_rainbow_hsl") and HSLToColor or HSVToColor)(
RealTime() * gs.GetPlayerSetting(pPlayer, "gs_rb655_lightsaber_rainbow_frequency") % 360,
gs.GetPlayerSetting(pPlayer, "gs_rb655_lightsaber_rainbow_saturation"),
gs.GetPlayerSetting(pPlayer, "gs_rb655_lightsaber_rainbow_value"))
end
end)
local function DrawSingleBlade(self, pPlayer, colBlade)
local vPos, vDir = self:GetSaberPosAng()
rb655_RenderBlade(vPos, vDir, self:GetBladeLength(), self:GetMaxLength(), self:GetBladeWidth(), colBlade, self:GetDarkInner(), self:EntIndex(), pPlayer:WaterLevel() > 2)
end
-- Code adapted from weapon_lightsaber.lua
local function DrawLightsaber(self)
do
local sModel = self:GetWorldModel()
self.WorldModel = sModel
self:SetModel(sModel)
end
self:DrawModel()
local pOwner = self:GetOwner()
if (not pOwner:IsValid() or halo.RenderedEntity() == self) then
return
end
local colBlade = hook.Run("gs_rb655_lightsaber_GetColor", self)
if (colBlade == nil or not gs.IsColor(colBlade)) then
colBlade = self:GetCrystalColor():ToColor()
end
local tAttachments = self:GetAttachments()
if (tAttachments == nil) then
DrawSingleBlade(self, pOwner, colBlade)
return
end
local uiAttachments = #tAttachments
if (uiAttachments == 0) then
DrawSingleBlade(self, pOwner, colBlade)
return
end
local bNoBlade = true
local uiBlades = 0
local dBladeLen = self:GetBladeLength()
local dMaxLen = self:GetMaxLength()
local dBladeWidth = self:GetBladeWidth()
local bBlackInner = self:GetDarkInner()
local uiEntIndex = self:EntIndex()
local bUnderwater = pOwner:WaterLevel() > 2
-- Cache loop functions
local string_match = string.match
local GetSaberPosAng = self.GetSaberPosAng
local rb655_RenderBlade = rb655_RenderBlade
for i = 1, #tAttachments do
local sAttachName = tAttachments[i].name
local uiBladeAttach = string_match(sAttachName, "^blade(%d+)$")
if (uiBladeAttach ~= nil) then
uiBlades = uiBlades + 1
local vPos, vDir = GetSaberPosAng(self, uiBladeAttach)
rb655_RenderBlade(vPos, vDir, dBladeLen, dMaxLen, dBladeWidth, colBlade, bBlackInner, uiEntIndex, bUnderwater, false, uiBlades)
bNoBlade = false
else
local uiQuillonAttach = string_match(sAttachName, "^quillon(%d+)$")
if (uiQuillonAttach ~= nil) then
uiBlades = uiBlades + 1
local vPos, vDir = GetSaberPosAng(self, uiBladeAttach)
rb655_RenderBlade(vPos, vDir, dBladeLen, dMaxLen, dBladeWidth, colBlade, bBlackInner, uiEntIndex, bUnderwater, true, uiBlades)
end
end
end
if (bNoBlade) then
local vPos, vDir = GetSaberPosAng(self)
rb655_RenderBlade(vPos, vDir, dBladeLen, dMaxLen, dBladeWidth, colBlade, bBlackInner, uiEntIndex, bUnderwater, false, uiBlades + 1)
end
end
-- Insert the modified functions before entities are created
-- FIXME: Use this instead when this has been merged: https://github.com/Facepunch/garrysmod/pull/1307
hook.Add("Initialize", "gs_rb655_lightsaber_rainbow", function()
local tWeapon = weapons.GetStored("weapon_lightsaber")
if (tWeapon == nil) then
gs.DevMsg(1, "weapon_lightsaber not registered, rainbow effects not implemented")
else
tWeapon.DrawWorldModel = DrawLightsaber
tWeapon.DrawWorldModelTranslucent = DrawLightsaber
gs.DevMsg(1, "weapon_lightsaber rainbow effects active")
end
end)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment