Last active
November 11, 2018 00:20
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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