Files
wnsrc/lua/stormfox2/lib/sh_settings.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

815 lines
24 KiB
Lua

--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[-------------------------------------------------------------------------
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
--[[<Shared>-----------------------------------------------------------------
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
--[[<Shared>------------------------------------------------------------------
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