--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] --[[------------------------------------------------------------------------- StormFox Settings Handle settings and convert convars. - Hooks: StormFox2.Setting.Change sName, vVarable ---------------------------------------------------------------------------]] StormFox2.Setting = {} -- Local functions and var local NET_ALLSETTINGS = 0 local NET_UPDATE = 1 local settingCallback = {} local settings = {} local cache = {} local ValueToString, StringToValue do function StringToValue( str, _type ) _type = _type:lower() if ( _type == "vector" ) then return Vector( str ) end if ( _type == "angle" ) then return Angle( str ) end if ( _type == "float" or _type == "number" ) then return tonumber( str ) end if ( _type == "int" ) then return math.Round( tonumber( str ) ) end if ( _type == "bool" or _type == "boolean" ) then return tobool( str ) end if ( _type == "string" ) then return tostring( str ) end if ( _type == "entity" ) then return Entity( str ) end StormFox2.Warning("Unable parse: " .. _type, true) end function ValueToString( var, _type ) _type = (_type or type( var )):lower() if _type == "vector" or _type == "angle" then return string.format( "%.2f %.2f %.2f", var:Unpack() ) end if _type == "number" or _type == "float" then return util.NiceFloat( var ) end if _type == "int" then var = math.Round( var ) end return tostring( var ) end end -- Load the settings local mapFile, defaultFile if SERVER then mapFile, defaultFile = "stormfox2/sv_settings/" .. game.GetMap() .. ".json", "stormfox2/sv_settings/default.json" else mapFile, defaultFile = "stormfox2/cl_settings/" .. game.GetMap() .. ".json", "stormfox2/cl_settings/default.json" end local settingsFile = file.Exists(mapFile, "DATA") and mapFile or defaultFile local fileData = {} if file.Exists(settingsFile, "DATA") then fileData = util.JSONToTable( file.Read(settingsFile, "DATA") or "" ) or {} end local blockSaveFile = false local function saveToFile() if blockSaveFile then return end local data = {} for sName, obj in pairs( settings ) do if CLIENT and obj:IsServer() then continue end -- If you're the client, ignore server settings if sName == "mapfile" then continue end if sName == "mapfile_cl" then continue end if obj:IsDefault() then continue end data[sName] = obj:GetString() end StormFox2.FileWrite( settingsFile, util.TableToJSON(data, true) ) end ---Returns faæse if we're saving to the default json file. ---@return boolean ---@shared function StormFox2.Setting.IsUsingMapFile() return settingsFile == mapFile end ---Enable/Disable saving to map-specific file. ---@param bool boolean ---@shared function StormFox2.Setting.UseMapFile( bool ) if bool then if settingsFile == mapFile then return end settingsFile = mapFile -- Settings only save once callback is done. Therefor we force this to save local ob = blockSaveFile blockSaveFile = false saveToFile() -- "Copy" the settings to the file blockSaveFile = ob else if settingsFile == defaultFile then return end file.Delete(mapFile) settingsFile = defaultFile -- Reload the default file fileData = util.JSONToTable( file.Read(settingsFile, "DATA") or "" ) or {} blockSaveFile = true for sName, var in pairs( fileData ) do local obj = StormFox2.Setting.GetObject( sName ) if not obj then continue end local newVar = fileData[sName] and StringToValue(fileData[sName], obj.type) obj:SetValue( newVar ) end blockSaveFile = false end end ---Forces SF2 to save the settings. ---@shared function StormFox2.Setting.ForceSave() blockSaveFile = false saveToFile() end ---Returns the file we're saving to. ---@return string function StormFox2.Setting.GetSaveFile() return settingsFile end -- Meta Table ---@class SF2Convar ---@field SetGroup function ---@field GetGroup function ---@field SetDescription function ---@field GetDescription function local meta = {} meta.__index = meta AccessorFunc(meta, "group", "Group") AccessorFunc(meta, "desc", "Description") function meta:GetName() return self.sName end function meta:GetValue() return self.value end function meta:GetDescription() return self.desc end function meta:IsSecret() return self.isSecret or false end function meta:SetValue( var ) if self:GetValue() == var then return self end -- Ignore StormFox2.Setting.Set(self:GetName(), var) return self end function meta:GetDefault() return self.default end function meta:IsDefault() return self:GetValue() == self:GetDefault() end function meta:Revert() self:SetValue( self:GetDefault() ) end function meta:IsServer() return self.server end function meta:GetType() return self.type end function meta:SetMenuType( sType, tSortOrter ) StormFox2.Setting.SetType( self:GetName(), sType, tSortOrter ) return self end function meta:GetMin() return self.min end function meta:GetMax() return self.max end function meta:GetString() return ValueToString(self:GetValue(), self:GetType()) end function meta:IsFuzzyOn() if not self:GetValue() then return false end if self.type == "number" then if self:GetValue() <= ( self:GetMin() or 0 ) then return false end end return true end function meta:SetFuzzyOn() if self.type == "boolean" then self:SetValue( true ) elseif self.type == "number" then local lowest = ( self:GetMin() or 0 ) self:SetValue( lowest + 1 ) end return self end function meta:SetFuzzyOff() if self.type == "boolean" then self:SetValue( false ) elseif self.type == "number" then local lowest = ( self:GetMin() or 0 ) self:SetValue( lowest ) end return self end function meta:SetFromString( str, type ) self:SetValue( StringToValue( str, type or self.type ) ) return self end function meta:AddCallback(fFunc,sID) StormFox2.Setting.Callback(self:GetName(),fFunc,sID) end -- Ties the setting to others. Does require all to be booleans or form of toggles do local radioTab = {} local radioTabDefault = {} local blockLoop = false local function callBack(vVar, oldVar, sName, id) if blockLoop then return end -- Another setting made you change. Don't run. local obj = settings[sName] if not obj then StormFox2.Warning("Invalid radio-setting!", true) end local a = radioTab[sName] if not a then return end -- Make sure we turned "on" if not obj:IsFuzzyOn() then -- We got turned off. Make sure at least one is on if not blockLoop then -- Turned off, and we didn't get called by others local default = a[1] -- First one in list. This is to ensure self never get set. for _, other in ipairs( a ) do if other:IsFuzzyOn() then return end -- One of the others are on. Ignore. if radioTabDefault[other:GetName()] then -- I'm the default one default = other end end -- All other settings are off. Try and switch the default on. if default:GetName() == sName then -- Tell the settings we can't be turned off return false else default:SetFuzzyOn() end end return end blockLoop = true -- Tell the others we turned on, and they have to turn off for _, other in ipairs( a ) do if other:GetName() == obj:GetName() then continue end -- Just to make sure we don't loop around other:SetFuzzyOff() end blockLoop = false end local callOthers = true -- Makes all settings turn off, if one of them are turned on. function meta:SetRadioAll( ... ) if self:IsServer() and CLIENT then -- Not your job to keep track. self._radioB = true for _, other in ipairs( { ... } ) do other._radioB = true end return self end local a = {} -- Make sure the arguments doesn't contain itself and all is from the same realm. local f = { ... } for _, other in ipairs( f ) do if other:GetName() == self:GetName() then continue end -- Don't include self if other:IsServer() ~= self:IsServer() then -- Make sure same realm StormFox2.Warning(other:GetName() .. " tried to tie itself to a setting from another realm!") continue end table.insert(a, other) end if #a < 1 then StormFox2.Warning(self:GetName() .. " tried to tie itself to nothing!",true) end -- Tell the other settings to do the same if callOthers then callOthers = false for _, other in ipairs( a ) do other:SetRadio( self, unpack( a ) ) end callOthers = true end radioTab[self:GetName()] = a StormFox2.Setting.Callback(self:GetName(),callBack,"radio_setting") return self end function meta:SetRadioDefault() -- Turn on if all others are off radioTabDefault[self:GetName()] = true return self end function meta:IsRadio() return (radioTab[self:GetName()] or self._radioB) and true or false end -- Tells these settings to turn off, if this setting is turned on function meta:SetRadio( ... ) if self:IsServer() and CLIENT then -- Not your job to keep track. self._radioB = true return self end local a = {} -- Make sure the arguments doesn't contain itself and all is from the same realm. for _, other in ipairs( { ... } ) do if other:GetName() == self:GetName() then continue end -- Don't include self if other:IsServer() ~= self:IsServer() then -- Make sure same realm StormFox2.Warning(other:GetName() .. " tried to tie itself to a setting from another realm!") continue end table.insert(a, other) end if #a < 1 then StormFox2.Warning(self:GetName() .. " tried to tie itself to nothing!",true) end radioTab[self:GetName()] = a StormFox2.Setting.Callback(self:GetName(),callBack,"radio_setting") return self end end local postSettingChace = {} -- Creates a setting and returns the setting-object ---Creates a server-side setting. Has to be called on the client to show up in the menu. ---@param sName string ---@param vDefaultVar any ---@param sDescription? string ---@param sGroup? string ---@param nMin? number ---@param nMax? number ---@return SF2Convar ---@shared function StormFox2.Setting.AddSV(sName,vDefaultVar,sDescription,sGroup, nMin, nMax) if settings[sName] then return settings[sName] end -- Already created if StormFox2.Map then vDefaultVar = StormFox2.Map.GetSetting( sName ) or vDefaultVar end local t = {} setmetatable(t, meta) t.sName = sName t.type = type(vDefaultVar) if SERVER then if fileData[sName] ~= nil then t.value = StringToValue(fileData[sName], t.type) end if t.value == nil then -- Check convar before setting the setting. local con = GetConVar("sf_" .. sName) if con then t.value = StringToValue(con:GetString(), t.type) end end if t.value == nil then -- If all fails, use the default t.value = vDefaultVar end else t.value = postSettingChace[sName] or vDefaultVar end t.default = vDefaultVar t.server = true t.min = nMin t.max = nMax t:SetGroup( sGroup ) t:SetDescription( sDescription ) settings[sName] = t return t end -- Creates a setting and returns the setting-object if CLIENT then ---Creates a client-side setting. ---@param sName string ---@param vDefaultVar any ---@param sDescription? string ---@param sGroup? string ---@param nMin? number ---@param nMax? number ---@return SF2Convar ---@client function StormFox2.Setting.AddCL(sName,vDefaultVar,sDescription,sGroup, nMin, nMax) if settings[sName] then return settings[sName] end -- Already added local t = {} setmetatable(t, meta) t.sName = sName t.type = type(vDefaultVar) if CLIENT then if fileData[sName] ~= nil then t.value = StringToValue(fileData[sName], t.type) end if t.value == nil then local con = GetConVar("sf_" .. sName) if con then t.value = StringToValue(con:GetString(), t.type) end end if t.value == nil then -- If all fails, use the default t.value = vDefaultVar end end t.default = vDefaultVar t.server = false t.min = nMin t.max = nMax t:SetGroup( sGroup ) t:SetDescription( sDescription ) settings[sName] = t return t end end ---Tries to onvert to the given defaultvar to match the setting. ---@param sName string ---@param sString string ---@return any ---@shared function StormFox2.Setting.StringToType( sName, sString ) local obj = settings[sName] if not obj then return sString end -- No idea return StringToValue( sString, obj.type ) end ---Returns a setting and will try to convert to the given defaultvar type. Fallback to vDefaultVar if nil. ---@param sName string ---@param vDefaultVar? any ---@return any ---@shared function StormFox2.Setting.Get(sName,vDefaultVar) local obj = settings[sName] if not obj then return vDefaultVar end return obj:GetValue() end ---Returns hte setting object. ---@param sName string ---@return SF2Convar ---@shared function StormFox2.Setting.GetObject(sName) return settings[sName] end --[[----------------------------------------------------------------- Sets a StormFox setting ---------------------------------------------------------------------------]] local w_list = { "openweathermap_key", "openweathermap_real_lat", "openweathermap_real_lon" } --value_type["openweathermap_key"] = "string" --value_type["openweathermap_real_lat"] = "string" --value_type["openweathermap_real_lon"] = "string" local function CallBack( sName, newVar, oldVar) if not settingCallback[sName] then return end for id, fFunc in pairs( settingCallback[sName] ) do if isstring(id) or IsValid(id) then fFunc(newVar, oldVar, sName, id) else -- Invalid settingCallback[sName][id] = nil end end end ---Tries to set a setting. ---@param sName string ---@param vVar any ---@param bDontSave boolean ---@return boolean saved function StormFox2.Setting.Set(sName,vVar, bDontSave) -- Check if valid local obj = settings[sName] if not obj then StormFox2.Warning("Invalid setting: " .. sName .. "!") return false end -- Check the variable if obj.type ~= type(vVar) then if type(vVar) == "string" then -- Try and convert it vVar = StringToValue( vVar, obj.type ) else StormFox2.Warning("Invalid variable: " .. sName .. "!") return false end if vVar == nil then return false end -- Failed end -- Check min and max if obj.type == "number" and obj.min then vVar = math.max(vVar, obj.min) end if obj.type == "number" and obj.max then vVar = math.min(vVar, obj.max) end -- Check for duplicates local oldVar = obj:GetValue() if oldVar == vVar then return end -- Same value, ignore -- We need to ask the server to change this setting. This isn't ours if CLIENT and obj:IsServer() then if StormFox2.Permission then StormFox2.Permission.RequestSetting(sName, vVar) else StormFox2.Warning("Unable to ask server to change: " .. sName .. "!") end return false end -- Save the value -- Make callbacks local oB = blockSaveFile -- Editing a setting, might change others. Only save after we're done blockSaveFile = true local oldVar = obj.value obj.value = vVar -- Callback CallBack(sName, vVar, oldVar) blockSaveFile = oB -- We're done changing settings, save if we can if not blockSaveFile and not bDontSave then if not (sName == "mapfile" or sName == "mapfile_cl") then saveToFile() end end cache[sName] = nil -- Delete cache -- Tell all clients about it if SERVER then if not obj:IsSecret() then net.Start( StormFox2.Net.Settings ) net.WriteUInt(NET_UPDATE, 3) net.WriteString(sName) net.WriteType(vVar) net.Broadcast() end end --[[------------------------------------------------------------------ Gets called when a StormFox setting changes. ---------------------------------------------------------------------------]] hook.Run("StormFox2.Setting.Change",sName,vVar, oldVar) return true end -- Server and Clientside NET if CLIENT then net.Receive(StormFox2.Net.Settings,function(len) local _type = net.ReadUInt(3) if _type == NET_UPDATE then local sName = net.ReadString() local var = net.ReadType() local obj = settings[sName] if not obj then StormFox2.Warning("Server tried to set an unknown setting: " .. sName) return end if not obj:IsServer() then StormFox2.Warning("Server tried to set a clientside setting: " .. sName) else local oldVar = obj.value obj.value = var cache[sName] = var -- Callback CallBack(sName, var, oldVar) hook.Run("StormFox2.Setting.Change",sName,obj.value,oldVar) end elseif _type == NET_ALLSETTINGS then local tab = net.ReadTable() -- I'm lazy for sName, vVar in pairs( tab ) do local obj = settings[sName] if not obj then -- So this setting "might" be used later. Cache the setting-value and ignore postSettingChace[sName] = vVar -- StormFox2.Warning("Server tried to set an unknown setting: " .. sName) continue end if not obj:IsServer() then -- This is a clientside setting. Nope.AVI StormFox2.Warning("Server tried to set a clientside setting: " .. sName) else local oldVar = obj.value obj.value = vVar cache[sName] = vVar -- Callback CallBack(sName, vVar, oldVar) hook.Run("StormFox2.Setting.Change",sName,obj.value,oldVar) end end end end) else hook.Add("StormFox2.data.initspawn", "StormFox2.setting.send", function( ply ) net.Start( StormFox2.Net.Settings ) net.WriteUInt(NET_ALLSETTINGS, 3) for sName, obj in pairs( settings ) do if not obj then continue end if obj:IsSecret() then continue end net.WriteType( sName ) net.WriteType( obj:GetValue() ) end -- End of table net.WriteType( nil ) net.Send(ply) end) end ---Calls the function when the given setting changes. ---fFunc will be called with: vNewVariable, vOldVariable, ConVarName, sID. ---Unlike convars, server-setings will also be triggered on the clients too. ---@param sName string ---@param fFunc function ---@param sID any ---@shared function StormFox2.Setting.Callback(sName,fFunc,sID) if not settingCallback[sName] then settingCallback[sName] = {} end settingCallback[sName][sID or "default"] = fFunc end --hook.Add("StormFox2.Setting.Change", "StormFox2.Setting.Callbacks", function(sName, vVar, oldVar) --if not settingCallback[sName] then return end --for id, fFunc in pairs( settingCallback[sName] ) do -- fFunc(vVar, oldVar, sName, id) --end --end) ---Same as StormFox2.Setting.Get, however this will cache the result. ---@param sName string ---@param vDefaultVar any ---@return any ---@shared function StormFox2.Setting.GetCache(sName,vDefaultVar) if cache[sName] then return cache[sName] end local var = StormFox2.Setting.Get(sName,vDefaultVar) cache[sName] = var return var end ---Returns the default setting ---@param sName string ---@return any ---@shared function StormFox2.Setting.GetDefault(sName) local obj = settings[sName] if not obj then return nil end return obj:GetDefault() end ---Returns all known settings. Clients will reitrhn both server and client settings. ---@return table ---@shared function StormFox2.Setting.GetAll() return table.GetKeys( settings ) end ---Returns all server settings. ---@return table ---@shared function StormFox2.Setting.GetAllServer() -- Server only has server settings if SERVER then return StormFox2.Setting.GetAll() end -- Make list local t = {} for sName,obj in pairs(settings) do if not obj:IsServer() then continue end table.insert(t, sName) end return t end if CLIENT then ---Returns all client settings. ---@return table function StormFox2.Setting.GetAllClient() local t = {} for sName,obj in pairs(settings) do if obj:IsServer() then continue end table.insert(t, sName) end return t end end -- Returns the valuetype of the setting. This can allow special types like tables .. ect local type_override = {} ---Returns the settigns variable type. ---@param sName string ---@return string ---@shared function StormFox2.Setting.GetType( sName ) if type_override[sName] then return type_override[sName] end local obj = settings[sName] if not obj then return end return obj.type end --[[ Type: - number - string - float - boolean - A table of options { [value] = "description" } - special_float Marks below 0 as "off" - time - temp / temperature - Time_toggle ]] ---Sets setting's variable type. Can also use special types like "time". ---@param sName string ---@param sType string ---@param tSortOrter table ---@shared function StormFox2.Setting.SetType( sName, sType, tSortOrter ) if type(sType) == "nil" then -- Reset it StormFox2.Warning("Can't make the setting a nil-type!") end if type(sType) == "boolean" then type_override[sName] = "boolean" elseif type(sType) == "number" then type_override[sName] = "number" elseif type(sType) == "table" then -- A table is a list of options type_override[sName] = {sType, tSortOrter} else if sType == "bool" then type_override[sName] = "boolean" else type_override[sName] = string.lower(sType) end end end -- Resets all stormfox settings to default. if SERVER then local obj = StormFox2.Setting.AddSV("mapfile", false) obj:AddCallback(StormFox2.Setting.UseMapFile) obj:SetValue( StormFox2.Setting.IsUsingMapFile() ) ---Returns all settings back to default. ---@server function StormFox2.Setting.Reset() blockSaveFile = true for sName, obj in pairs(settings) do if sName == "mapfile" then continue end obj:Revert() end blockSaveFile = false saveToFile() StormFox2.Warning("All settings were reset to default values. You should restart!") cache = {} end else StormFox2.Setting.AddSV("mapfile", false) local obj = StormFox2.Setting.AddCL("mapfile_cl", false) obj:AddCallback(StormFox2.Setting.UseMapFile) obj:SetValue( StormFox2.Setting.IsUsingMapFile() ) ---Returns all settings back to default. ---@client function StormFox2.Setting.Reset() blockSaveFile = true for _, sName in ipairs(StormFox2.Setting.GetAllClient()) do if sName == "mapfile_cl" then continue end local obj = setting[sName] if not obj then continue end obj:Revert() end blockSaveFile = false saveToFile() StormFox2.Warning("All settings were reset to default values. You should rejoin!") cache = {} end end -- Gets and sets StormFox server setting if SERVER then ---Parses a CVS string and applies all settings to SF2. ---@param str string ---@server function StormFox2.Setting.SetCVS( str ) local t = string.Explode(",", str) blockSaveFile = true for i = 1, #t, 2 do local sName, var = t[i], t[i+1] or nil if string.len(sName) < 1 or not var then continue end local obj = StormFox2.Setting.GetObject(sName ) if not obj then StormFox2.Warning("Invalid setting: " .. sName .. ".") continue else obj:SetValue(var) end end blockSaveFile = false saveToFile() StormFox2.Warning("All settings were updated. You should restart!") end end local exlist = {"openweathermap_real_lat", "openweathermap_real_lon", "openweathermap_key"} ---Compiles all server-settigns into a CVS string. ---@return string ---@shared function StormFox2.Setting.GetCVS() local c = "" for sName, obj in pairs(settings) do if obj:IsSecret() then continue end if not obj:IsServer() then continue end c = c .. sName .. "," .. obj:GetString() .. "," end return c end ---Compiles all default server-settings into a CVS string. ---@return string ---@shared function StormFox2.Setting.GetCVSDefault() local c = "" for sName, obj in pairs(settings) do if obj:IsSecret() then continue end -- Justi n case, so people don't share hidden settings if not obj:IsServer() then continue end c = c .. sName .. "," .. ValueToString(obj:GetDefault(), obj:GetType()) .. "," end return c end -- Disable SF2 StormFox2.Setting.AddSV("enable", true, nil, "Start") StormFox2.Setting.AddSV("allow_csenable", engine.ActiveGamemode() == "sandbox", nil, "Start") if CLIENT then StormFox2.Setting.AddCL("clenable", true, nil, "Start") end ---Returns true if SF2 is enabled. ---@return boolean ---@shared function StormFox2.Setting.SFEnabled() if not StormFox2.Setting.GetCache("enable", true) then return false end if SERVER or not StormFox2.Setting.GetCache("allow_csenable", false) then return true end return StormFox2.Setting.GetCache("clenable", true) end