mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
901 lines
30 KiB
Lua
901 lines
30 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/
|
|
--]]
|
|
|
|
|
|
StormFox2.WeatherGen = StormFox2.WeatherGen or {}
|
|
-- Settings
|
|
local auto_weather = StormFox2.Setting.AddSV("auto_weather",true,nil, "Weather")
|
|
local hide_forecast = StormFox2.Setting.AddSV("hide_forecast",false,nil, "Weather")
|
|
util.AddNetworkString( "StormFox2.weekweather" )
|
|
|
|
local forecast = {} -- The forecast table
|
|
local function SetForecast( tab, unix_time )
|
|
forecast = tab
|
|
forecast.unix_time = unix_time
|
|
if not hide_forecast then return end
|
|
net.Start("StormFox2.weekweather")
|
|
net.WriteBool( unix_time )
|
|
net.WriteTable( tab.temperature or {} )
|
|
net.WriteTable( tab.weather or {} )
|
|
net.WriteTable( tab.wind or {} )
|
|
net.WriteTable( tab.windyaw or {} )
|
|
net.Broadcast()
|
|
end
|
|
|
|
---Returns the forecast data
|
|
---@return table
|
|
---@server
|
|
function StormFox2.WeatherGen.GetForecast()
|
|
return forecast
|
|
end
|
|
|
|
---Returns true if we're using unix time for the forecast.
|
|
---@return boolean
|
|
---@server
|
|
function StormFox2.WeatherGen.IsUnixTime()
|
|
return forecast.unix_time or false
|
|
end
|
|
|
|
-- Open Weather API Settings
|
|
local api_MaxCalls = 59
|
|
|
|
local KEY_INVALID = 0
|
|
local KEY_UNKNOWN = 1
|
|
local KEY_VALID = 2
|
|
|
|
local KEY_STATUS = KEY_UNKNOWN
|
|
|
|
local API_ENABLE = StormFox2.Setting.AddSV("openweathermap_enabled",false,nil,"Weather")
|
|
StormFox2.Setting.AddSV("openweathermap_lat",52,nil,"Weather",-180,180) -- Fake setting
|
|
StormFox2.Setting.AddSV("openweathermap_lon",-2,nil,"Weather",-180,180) -- Fake setting
|
|
|
|
if SERVER then
|
|
CreateConVar("sf_openweathermap_key", "", {FCVAR_ARCHIVE, FCVAR_PROTECTED}, "Sets the API key")
|
|
CreateConVar("sf_openweathermap_real_lat","52.613909" , {FCVAR_ARCHIVE, FCVAR_PROTECTED}, "The real LAT for the API")
|
|
CreateConVar("sf_openweathermap_real_lon","-2.005960" , {FCVAR_ARCHIVE, FCVAR_PROTECTED}, "The real LON for the API")
|
|
end
|
|
local key = StormFox2.Setting.AddSV("openweathermap_key", "", nil,"Weather")
|
|
local location = StormFox2.Setting.AddSV("openweathermap_location", "", nil,"Weather") -- Fake setting
|
|
local city = StormFox2.Setting.AddSV("openweathermap_city","",nil,"Weather") -- Fake setting
|
|
|
|
-- Keep them secret and never network them.
|
|
key.isSecret = true
|
|
location.isSecret = true
|
|
city.isSecret = true
|
|
local function onSuccessF( body, len, head, code )
|
|
KEY_STATUS = KEY_VALID
|
|
if not auto_weather:GetValue() then return end
|
|
-- Most likly an invalid API-Key.
|
|
local t = util.JSONToTable(body) or {}
|
|
if code == 401 then
|
|
KEY_STATUS = KEY_INVALID
|
|
StormFox2.Warning(t.message or "API returned 401! Check your OpenWeatherMap account.")
|
|
StormFox2.Setting.Set("openweathermap_enabled", false)
|
|
return
|
|
end
|
|
if t.cod == "404" then return end -- Not found
|
|
local timeZone = t.timezone and tonumber(t.timezone) or 0
|
|
-- We can set the sunrise and sunset
|
|
if t.sys and t.sys.sunrise and t.sys.sunset then
|
|
local sunrise = os.date("!%H:%M",t.sys.sunrise + timeZone)
|
|
local sunset = os.date("!%H:%M",t.sys.sunset + timeZone)
|
|
StormFox2.Setting.Set("sunrise",StormFox2.Time.StringToTime(sunrise))
|
|
StormFox2.Setting.Set("sunset",StormFox2.Time.StringToTime(sunset))
|
|
end
|
|
if t.main then
|
|
-- Temperature
|
|
local temp = StormFox2.Temperature.Convert("kelvin",nil,tonumber( t.main.temp or t.main.temp_min or t.main.temp_max ))
|
|
StormFox2.Temperature.Set( temp, 1 )
|
|
-- Weather
|
|
local cloudyness = ( t.clouds and t.clouds.all or 0 ) / 110
|
|
local rain = 0
|
|
if t.rain then
|
|
rain = math.max( t.rain["1h"] or 0, t.rain["3h"] or 0, 2) / 8
|
|
elseif t.snow then
|
|
rain = math.max( t.snow["1h"] or 0, t.snow["3h"] or 0, 2) / 8
|
|
end
|
|
if rain > 0 then
|
|
StormFox2.Weather.Set("Rain", math.Round(rain * .7 + 0.2,2))
|
|
elseif cloudyness >= 0.1 then
|
|
StormFox2.Weather.Set("Cloud", math.Round(cloudyness, 2))
|
|
else
|
|
StormFox2.Weather.Set("Clear", 1)
|
|
end
|
|
-- Thunder
|
|
local b_thunder = false
|
|
if t.weather and t.weather[1] and t.weather[1].id and (rain > 0 or cloudyness >= 0.3) then
|
|
local id = t.weather[1].id
|
|
b_thunder = ( id >= 200 and id <= 202 ) or ( id >= 210 and id <= 212 ) or ( id >= 230 and id <= 232 ) or id == 212
|
|
end
|
|
StormFox2.Thunder.SetEnabled(b_thunder, id == 212 and 12 or 6) -- 212 is heavy thunderstorm
|
|
-- Wind
|
|
StormFox2.Wind.SetForce( t.wind and t.wind.speed or 0 )
|
|
StormFox2.Wind.SetYaw( t.wind and t.wind.deg or 0 )
|
|
end
|
|
end
|
|
local n_NextAllowedCall = 0
|
|
local b_BlockNextW = false
|
|
local function UpdateLiveWeather( api_key )
|
|
if b_BlockNextW then return end
|
|
if KEY_STATUS == KEY_INVALID then return end
|
|
if n_NextAllowedCall >= CurTime() then
|
|
return StormFox2.Warning("API can't be called that often!")
|
|
end
|
|
n_NextAllowedCall = CurTime() + (60 / api_MaxCalls)
|
|
local lat = GetConVar("sf_openweathermap_real_lat"):GetString()
|
|
local lon = GetConVar("sf_openweathermap_real_lon"):GetString()
|
|
local api_key = api_key or GetConVar("sf_openweathermap_key"):GetString()
|
|
http.Fetch("http://api.openweathermap.org/data/2.5/weather?lat=" .. lat .. "&lon=" .. lon .. "&appid=" .. api_key, onSuccessF)
|
|
end
|
|
|
|
local function onSuccessForecast( body, len, head, code )
|
|
if KEY_STATUS == KEY_INVALID then return end
|
|
local t = util.JSONToTable(body) or {}
|
|
if code == 401 then
|
|
KEY_STATUS = KEY_INVALID
|
|
StormFox2.Warning(t.message or "API returned 401! Check your OpenWeatherMap account.")
|
|
StormFox2.Setting.Set("openweathermap_enabled", false)
|
|
return
|
|
end
|
|
if t.cod == "404" then return end -- Not found
|
|
if not t.list then return end -- ??
|
|
local forecast = {}
|
|
forecast.temperature= {}
|
|
forecast.weather = {}
|
|
forecast.wind = {}
|
|
forecast.windyaw = {}
|
|
|
|
local last_time = -1
|
|
local ex_time = 0
|
|
for k, v in ipairs( t.list ) do
|
|
if not v.main then continue end -- ERR
|
|
--local timestr = string.match(v.dt_txt, "(%d+:%d+:%d)")
|
|
--local c = string.Explode(":", timestr)
|
|
--local h, m, s = c[1] or "0", c[2] or "0", c[3] or "0"
|
|
--local time = ( tonumber( h ) or 0 ) * 60 + (tonumber( m ) or 0) + (tonumber( s ) or 0) / 60
|
|
--if time < last_time then -- New day
|
|
-- ex_time = ex_time + 1440
|
|
-- last_time = time
|
|
--else
|
|
-- last_time = time
|
|
--end
|
|
-- New time: time + ex_time
|
|
--local timeStamp = time + ex_time
|
|
local timeStamp = v.dt
|
|
if timeStamp > 1440 then
|
|
if k%2 == 1 then
|
|
continue
|
|
end
|
|
elseif timeStamp > 2440 then
|
|
continue
|
|
end
|
|
|
|
local temp = StormFox2.Temperature.Convert("kelvin",nil,tonumber( v.main.temp or v.main.temp_min or v.main.temp_max ))
|
|
|
|
local cloudyness = ( t.clouds and t.clouds.all or 0 ) / 110
|
|
local rain = 0
|
|
local w_type = "Clear"
|
|
local w_procent = 0
|
|
if v.rain then
|
|
rain = math.max( v.rain["1h"] or 0, v.rain["3h"] or 0, 2) / 8
|
|
elseif v.snow then
|
|
rain = math.max( v.snow["1h"] or 0, v.snow["3h"] or 0, 2) / 8
|
|
end
|
|
if rain > 0 then
|
|
w_procent = math.Round(rain * .7 + 0.2,2)
|
|
w_type = "Rain"
|
|
elseif cloudyness > 0.1 then
|
|
w_procent = math.Round(cloudyness, 2)
|
|
w_type = "Cloud"
|
|
end
|
|
local b_thunder = false
|
|
if v.weather and v.weather[1] and v.weather[1].id and (rain > 0 or cloudyness >= 0.3) then
|
|
local id = v.weather[1].id
|
|
b_thunder = ( id >= 200 and id <= 202 ) or ( id >= 210 and id <= 212 ) or ( id >= 230 and id <= 232 ) or id == 212
|
|
end
|
|
local wind = v.wind and v.wind.speed or 0
|
|
local windyaw = v.wind and v.wind.deg or 0
|
|
|
|
table.insert(forecast.temperature, {timeStamp, temp})
|
|
table.insert(forecast.weather, {timeStamp, {
|
|
["sName"] = w_type,
|
|
["fAmount"] = w_procent
|
|
}})
|
|
table.insert(forecast.wind, {timeStamp, wind})
|
|
table.insert(forecast.windyaw, {timeStamp, windyaw})
|
|
end
|
|
SetForecast( forecast, true )
|
|
end
|
|
|
|
local function UpdateLiveFeed( api_key )
|
|
if KEY_STATUS == KEY_INVALID then return end
|
|
local lat = GetConVar("sf_openweathermap_real_lat"):GetString()
|
|
local lon = GetConVar("sf_openweathermap_real_lon"):GetString()
|
|
local api_key = api_key or GetConVar("sf_openweathermap_key"):GetString()
|
|
http.Fetch("http://api.openweathermap.org/data/2.5/forecast?lat=" .. lat .. "&lon=" .. lon .. "&appid=" .. api_key, onSuccessForecast)
|
|
end
|
|
|
|
local function SetCity( sCityName, callBack )
|
|
if KEY_STATUS == KEY_INVALID then return end
|
|
if n_NextAllowedCall >= CurTime() then
|
|
return StormFox2.Warning("API can't be called that often!")
|
|
end
|
|
n_NextAllowedCall = CurTime() + (60 / api_MaxCalls)
|
|
http.Fetch("http://api.openweathermap.org/data/2.5/weather?q=" .. sCityName .. "&appid=" .. GetConVar("sf_openweathermap_key"):GetString(), function( body, len, head, code )
|
|
-- Most likly an invalid API-Key.
|
|
local t = util.JSONToTable(body) or {}
|
|
if code == 401 then
|
|
KEY_STATUS = KEY_INVALID
|
|
StormFox2.Warning(t.message or "API returned 401")
|
|
StormFox2.Setting.Set("openweathermap_enabled", false)
|
|
return
|
|
end
|
|
if t.cod == 404 or not t.coord then -- City not found
|
|
if callBack then callBack( false ) end
|
|
return
|
|
end
|
|
b_BlockNextW = true -- Stop the setting from updating the weather again
|
|
local lat = tonumber( t.coord.lat )
|
|
RunConsoleCommand( "sf_openweathermap_real_lat", lat )
|
|
StormFox2.Setting.Set("openweathermap_lat",math.Round(lat)) -- Fake settings
|
|
|
|
local lon = tonumber( t.coord.lon )
|
|
RunConsoleCommand( "sf_openweathermap_real_lon", lon )
|
|
StormFox2.Setting.Set("openweathermap_lon",math.Round(lon)) -- Fake settings
|
|
b_BlockNextW = false
|
|
onSuccessF( body, len, head, code )
|
|
|
|
-- We found a city. Make the forecast
|
|
timer.Simple(1, UpdateLiveFeed)
|
|
if callBack then callBack( true ) end
|
|
end)
|
|
end
|
|
--- Update Value
|
|
key:AddCallback( function( sString )
|
|
RunConsoleCommand( "sf_openweathermap_key", sString )
|
|
key.value = "" -- Silent set it again
|
|
KEY_STATUS = KEY_UNKNOWN
|
|
UpdateLiveWeather( sString ) -- Try and set the weather
|
|
end)
|
|
location:AddCallback( function( sString )
|
|
local num = tonumber( string.match(sString, "[-%d]+") or "0" ) or 0
|
|
if sString:sub(0, 1) == "a" then
|
|
RunConsoleCommand( "sf_openweathermap_real_lat", num )
|
|
StormFox2.Setting.Set("openweathermap_lat",math.Round(num)) -- Fake settings
|
|
else
|
|
RunConsoleCommand( "sf_openweathermap_real_lon", num )
|
|
StormFox2.Setting.Set("openweathermap_lon",math.Round(num)) -- Fake settings
|
|
end
|
|
location.value = "" -- Silent set it again
|
|
UpdateLiveWeather() -- Set the weather to the given location
|
|
timer.Simple(1, UpdateLiveFeed)
|
|
end)
|
|
city:AddCallback( function( cityName )
|
|
if cityName == "" then return end
|
|
SetCity( cityName )
|
|
city.value = "" -- Silent set it again
|
|
end)
|
|
|
|
-- Enable and disable API
|
|
local status = false
|
|
local function EnableAPI()
|
|
if status then return end
|
|
timer.Create("SF_WGEN_API", 5 * 60, 0, function()
|
|
if not auto_weather:GetValue() then return end
|
|
if not status then return end
|
|
UpdateLiveWeather()
|
|
end)
|
|
timer.Simple(1, UpdateLiveFeed)
|
|
end
|
|
local function DisableAPI()
|
|
if not status then return end
|
|
timer.Destroy("SF_WGEN_API")
|
|
end
|
|
local function IsUsingAPI()
|
|
return status
|
|
end
|
|
|
|
-- Weather Gen Settings
|
|
local max_days_generate = 7
|
|
local min_temp = StormFox2.Setting.AddSV("min_temp",-10,nil,"Weather",-273.15)
|
|
local max_temp = StormFox2.Setting.AddSV("max_temp",20,nil, "Weather")
|
|
local max_wind = StormFox2.Setting.AddSV("max_wind",50,nil, "Weather")
|
|
local night_temp= StormFox2.Setting.AddSV("addnight_temp",-7,nil, "Weather")
|
|
local function toStr( num )
|
|
local c = tostring( num )
|
|
return string.rep("0", 4 - #c) .. c
|
|
end
|
|
local default
|
|
local function SplitSetting( str )
|
|
if #str< 20 then return default end -- Invalid, use default
|
|
local tab = {}
|
|
local min = math.min(100, string.byte(str, 1,1) - 33 ) / 100
|
|
local max = math.min(100, string.byte(str, 2,2) - 33 ) / 100
|
|
tab.amount_min = math.min(min, max)
|
|
tab.amount_max = math.max(min, max)
|
|
|
|
local min = math.min(1440,tonumber( string.sub(str, 3, 6) ) or 0)
|
|
local max = math.min(1440,tonumber( string.sub(str, 7, 10) ) or 0)
|
|
tab.start_min = math.min(min, max)
|
|
tab.start_max = math.max(min, max)
|
|
|
|
local min = tonumber( string.sub(str, 11, 14) ) or 0
|
|
local max = tonumber( string.sub(str, 15, 18) ) or 0
|
|
|
|
tab.length_min = math.min(min, max)
|
|
tab.length_max = math.max(min, max)
|
|
|
|
tab.thunder = string.sub(str, 19, 19) == "1"
|
|
tab.pr_week = tonumber( string.sub(str, 20) ) or 0
|
|
return tab
|
|
end
|
|
local function CombineSetting( tab )
|
|
local c =string.char( 33 + (tab.amount_min or 0) * 100 )
|
|
c = c .. string.char( 33 + (tab.amount_max or 0) * 100 )
|
|
|
|
c = c .. toStr(math.Clamp( math.Round( tab.start_min or 0), 0, 1440 ) )
|
|
c = c .. toStr(math.Clamp( math.Round( tab.start_max or 0), 0, 1440 ) )
|
|
|
|
c = c .. toStr(math.Clamp( math.Round( tab.length_min or 360 ), 180, 9999) )
|
|
c = c .. toStr(math.Clamp( math.Round( tab.length_max or 360 ), 180, 9999) )
|
|
|
|
c = c .. (tab.thunder and "1" or "0")
|
|
|
|
c = c .. tostring( tab.pr_week or 2 )
|
|
return c
|
|
end
|
|
local default_setting = {}
|
|
default_setting["Rain"] = CombineSetting({
|
|
["amount_min"] = 0.4,
|
|
["amount_max"] = 0.9,
|
|
["start_min"] = 300,
|
|
["start_max"] = 1200,
|
|
["length_min"] = 360,
|
|
["length_max"] = 1200,
|
|
["thunder"] = true,
|
|
["pr_week"] = 3
|
|
})
|
|
default_setting["Cloud"] = CombineSetting({
|
|
["amount_min"] = 0.2,
|
|
["amount_max"] = 0.7,
|
|
["start_min"] = 300,
|
|
["start_max"] = 1200,
|
|
["length_min"] = 360,
|
|
["length_max"] = 1200,
|
|
["pr_week"] = 3
|
|
})
|
|
default_setting["Clear"] = CombineSetting({
|
|
["amount_min"] = 1,
|
|
["amount_max"] = 1,
|
|
["start_min"] = 0,
|
|
["start_max"] = 1440,
|
|
["length_min"] = 360,
|
|
["length_max"] = 1440,
|
|
["pr_week"] = 7
|
|
})
|
|
-- Morning fog
|
|
default_setting["Fog"] = CombineSetting({
|
|
["amount_min"] = 0.15,
|
|
["amount_max"] = 0.30,
|
|
["start_min"] = 360,
|
|
["start_max"] = 560,
|
|
["length_min"] = 160,
|
|
["length_max"] = 360,
|
|
["pr_week"] = 1
|
|
})
|
|
default = CombineSetting({
|
|
["amount_min"] = 0.4,
|
|
["amount_max"] = 0.9,
|
|
["start_min"] = 300,
|
|
["start_max"] = 1200,
|
|
["length_min"] = 300,
|
|
["length_max"] = 1200,
|
|
["pr_week"] = 0
|
|
})
|
|
-- Create settings for weather-types.
|
|
local weather_setting = {}
|
|
local OnWeatherSettingChange
|
|
local function call( newVar, oldVar, sName )
|
|
sName = string.match(sName, "wgen_(.+)") or sName
|
|
weather_setting[sName] = SplitSetting( newVar )
|
|
OnWeatherSettingChange()
|
|
end
|
|
hook.Add("stormfox2.postloadweather", "StormFox2.WeatherGen.Load", function()
|
|
for _, sName in ipairs( StormFox2.Weather.GetAll() ) do
|
|
local str = default_setting[sName] or default
|
|
local obj = StormFox2.Setting.AddSV("wgen_" .. sName,str,nil,"Weather")
|
|
obj:AddCallback( call )
|
|
weather_setting[sName] = SplitSetting( obj:GetValue() )
|
|
end
|
|
end)
|
|
for _, sName in ipairs( StormFox2.Weather.GetAll() ) do
|
|
local str = default_setting[sName] or default
|
|
local obj = StormFox2.Setting.AddSV("wgen_" .. sName,str,nil,"Weather")
|
|
obj:AddCallback( call, "updateWSetting" )
|
|
weather_setting[sName] = SplitSetting( obj:GetValue() )
|
|
end
|
|
local function SetWeatherSetting(sName, tab)
|
|
if CLIENT then return end
|
|
StormFox2.Setting.SetValue("wgen_" .. sName, CombineSetting(tab))
|
|
end
|
|
-- Returns the lowest key that is higer than the inputed key.
|
|
-- Second return is the higest key that is lower than the inputed key.
|
|
local function getClosestKey( tab, key)
|
|
local s, ls
|
|
for k, v in ipairs( table.GetKeys(tab) ) do
|
|
if v < key then
|
|
if not ls or ls < v then
|
|
ls = v
|
|
end
|
|
else
|
|
if not s or s > v then
|
|
s = v
|
|
end
|
|
end
|
|
end
|
|
return s, ls or 0
|
|
end
|
|
local generator = {} -- Holds all the days
|
|
local day = {}
|
|
day.__index = day
|
|
local function CreateDay()
|
|
local t = {}
|
|
t._temperature = {}
|
|
t._wind = {}
|
|
t._windyaw = {}
|
|
t._weather = {}
|
|
setmetatable(t, day)
|
|
table.insert(generator, t)
|
|
if #generator > max_days_generate then
|
|
table.remove(generator, 1)
|
|
end
|
|
return t
|
|
end
|
|
local lastTemp
|
|
function day:SetTemperature( nTime, nCelcius )
|
|
-- I got no clue why it tries to set values outside, but clamp it here just in case.
|
|
nCelcius = math.Clamp(nCelcius, min_temp:GetValue(), max_temp:GetValue())
|
|
self._temperature[nTime] = nCelcius
|
|
lastTemp = nCelcius
|
|
return self
|
|
end
|
|
function day:GetTemperature( nTime )
|
|
return self._temperature[nTime]
|
|
end
|
|
local lastWind, lastWindYaw
|
|
local lastLastWind
|
|
function day:SetWind( nTime, nWind, nWindYaw )
|
|
self._wind[nTime] = nWind
|
|
self._windyaw[nTime] = nWindYaw
|
|
lastLastWind = lastWind
|
|
lastWind = nWind
|
|
lastWindYaw = nWindYaw
|
|
return self
|
|
end
|
|
function day:GetWind( nTime )
|
|
return self._wind[nTime], self._windyaw[nTime]
|
|
end
|
|
function day:SetWeather( sName, nStart, nDuration, nAmount, nThunder )
|
|
self._weather[nStart] = {
|
|
["sName"] = sName,
|
|
["nStart"] = nStart,
|
|
["nDuration"] = nDuration,
|
|
["fAmount"] = nAmount,
|
|
["nThunder"] = nThunder }
|
|
self._last = math.max(self._last or 0, nStart + nDuration)
|
|
end
|
|
function day:GetWeather( nTime )
|
|
return self._weather[nTime]
|
|
end
|
|
function day:GetWeathers()
|
|
return self._weather
|
|
end
|
|
function day:GetLastWeather()
|
|
return self._weather[self._last]
|
|
end
|
|
local function GetLastDay()
|
|
return generator[#generator]
|
|
end
|
|
local weatherWeekCount = {}
|
|
local function CanGenerateWeather( sName )
|
|
local count = weatherWeekCount[ sName ] or 0
|
|
local setting = weather_setting[ sName ]
|
|
if not setting then return false end -- Invalid weahter? Ignore this.
|
|
local pr_week = setting.pr_week or 0
|
|
if pr_week <= 0 then return false end -- Disabled
|
|
if pr_week < 1 then -- Floats between 0 and 1 is random.
|
|
pr_week = math.random(0, 1 / pr_week) <= 1 and 1 or 0
|
|
end
|
|
if count >= pr_week then return false end -- This weahter is reached max for this week
|
|
return true
|
|
end
|
|
local function SortList()
|
|
local t = {}
|
|
for _, sName in pairs(StormFox2.Weather.GetAll()) do
|
|
t[sName] = weatherWeekCount[sName] or 0
|
|
end
|
|
return table.SortByKey(t, true)
|
|
end
|
|
local function UpdateWeekCount()
|
|
weatherWeekCount = {}
|
|
for k, day in ipairs( generator ) do
|
|
for nTime, weather in pairs( day:GetWeathers() ) do
|
|
local sName = weather.sName
|
|
weatherWeekCount[sName] = (weatherWeekCount[sName] or 0) + 1
|
|
end
|
|
end
|
|
end
|
|
local atemp = math.random(-4, 4)
|
|
local nextWeatherOverflow = 0
|
|
local function GenerateDay()
|
|
local newDay = CreateDay()
|
|
UpdateWeekCount()
|
|
-- Handle temperature
|
|
do
|
|
local mi, ma = min_temp:GetValue(), max_temp:GetValue()
|
|
local ltemp = lastTemp or math.random(mi, ma)
|
|
local aftemp = -math.min(ltemp - mi, 12) -- The closer the temperature is to minimum, the lower min
|
|
local ahtemp = math.min(ma - ltemp, 12) -- The closer the temperature is to maximum, the lower max
|
|
atemp = atemp + math.Rand(-4, 4) -- How much the temperature goes up or down
|
|
atemp = math.Clamp(atemp, aftemp, ahtemp)
|
|
-- UnZero
|
|
local tempBoost = 7 - math.abs( ltemp + atemp )
|
|
if tempBoost > 0 then
|
|
if atemp >= 0 then
|
|
atemp = math.max(atemp + tempBoost, tempBoost)
|
|
else
|
|
atemp = math.min(atemp - tempBoost, -tempBoost)
|
|
end
|
|
end
|
|
-- Spikes
|
|
if math.random(10) > 8 then
|
|
-- Create a spike
|
|
if ltemp + atemp >= 0 then
|
|
atemp = mi / 2
|
|
else
|
|
atemp = ma / 2
|
|
end
|
|
end
|
|
-- New temp
|
|
local newMidTemp = math.Round(math.Clamp(ltemp + atemp, mi + 4, ma), 1)
|
|
-- Make the new temperature
|
|
|
|
local h = StormFox2.Sun.GetSunRise()
|
|
local n_temp = night_temp:GetValue() or -7
|
|
local sunDown = StormFox2.Sun.GetSunSet() + math.random(-180, 180) - 180
|
|
newDay:SetTemperature( sunDown, newMidTemp )
|
|
newDay:SetTemperature( h - 180, math.max(newMidTemp + math.random(n_temp / 2, n_temp), mi) )
|
|
lastTemp = newMidTemp -- To make sure night-temp, don't effect the overall temp
|
|
end
|
|
-- Handle wind
|
|
local newWind
|
|
do
|
|
--lastWind, lastWindYaw
|
|
if not lastWind then
|
|
lastWind = math.random(5)
|
|
lastWindYaw = math.random(360)
|
|
end
|
|
local buff = math.abs(atemp) - 4 -- Wind tent to increase the more temp changes. Also add a small negative modifier
|
|
if math.random(1, 50) >= 49 and buff > 4 then -- Sudden Storm
|
|
buff = buff + 10
|
|
end
|
|
local addforce = math.random(buff / 2, buff - math.abs(lastLastWind or 0))
|
|
newWind = math.min(max_wind:GetValue(), math.max(0, lastWind + addforce))
|
|
local yawChange = math.min(40, lastWind + addforce * 15)
|
|
newDay:SetWind( math.random(180, 1080), newWind, ( lastWindYaw + math.random(-yawChange, yawChange) ) % 360 )
|
|
end
|
|
-- Handle weather
|
|
local i = 3 -- Only generates 2 types of weathers pr day at max
|
|
local _last = nextWeatherOverflow -- The next empty-time of the day
|
|
for _, sName in ipairs( SortList() ) do
|
|
if _last >= 1440 then continue end -- This day is full of weathers. Ignore.
|
|
if sName == "Clear" and math.random(0, newWind) < newWind * 0.8 then -- Roll a dice between 0 and windForce. If dice is below 80%, try and find another weahter instead.
|
|
if atemp > 0 then -- Warm weather tent to clear up the weather
|
|
break
|
|
elseif atemp < 0 then -- Colder weather will form weather
|
|
continue
|
|
end
|
|
end
|
|
-- Check if weather is enabled, and we haven't reached max.
|
|
if not CanGenerateWeather( sName ) then continue end
|
|
local setting = weather_setting[sName]
|
|
local minS, maxS = setting.start_min, setting.start_max
|
|
local minL, maxL = setting.length_min, setting.length_max
|
|
if _last >= maxS then continue end -- This weather can't be generated this late.
|
|
i = i - 1
|
|
if i <= 0 then break end
|
|
local start_time = math.random(math.max(minS, _last), maxS)
|
|
local length_time = math.random(minL, maxL)
|
|
local amount = math.Rand(setting.amount_min, setting.amount_max)
|
|
local nThunder
|
|
if setting.thunder and amount > 0.5 and math.random(0, 10) > 7 then
|
|
nThunder = math.random(4,8)
|
|
end
|
|
newDay:SetWeather( sName, start_time, length_time, amount, nThunder )
|
|
_last = start_time + length_time
|
|
end
|
|
nextWeatherOverflow = math.max(0, _last - 1440)
|
|
end
|
|
local function GenerateWeek()
|
|
for i = 1, max_days_generate do
|
|
GenerateDay()
|
|
end
|
|
end
|
|
local enable = false
|
|
local function IsUsingWeatherGen()
|
|
return enable
|
|
end
|
|
local function TimeToIndex( tab )
|
|
local c = table.GetKeys( tab )
|
|
local a = {}
|
|
table.sort(c)
|
|
for i = 1, #c do
|
|
a[i] = {c[i], tab[c[i]]}
|
|
end
|
|
return a
|
|
end
|
|
local wGenList = {}
|
|
local function PushDayToList() -- Merges the 7 days into one long line. Its more stable this way
|
|
-- empty forecast
|
|
wGenList = {}
|
|
wGenList._temperature = {}
|
|
wGenList._wind = {}
|
|
wGenList._windyaw = {}
|
|
wGenList._weather = {}
|
|
local lastWType = "Clear"
|
|
local lastWTime = -100
|
|
for i = 1, 4 do
|
|
local f = {}
|
|
local day = generator[i]
|
|
for nTime, var in pairs( day._temperature ) do
|
|
wGenList._temperature[ nTime + (i - 1) * 1440 ] = var
|
|
end
|
|
for nTime, var in pairs( day._wind ) do
|
|
local nn = nTime + (i - 1) * 1440
|
|
wGenList._wind[ nn ] = var
|
|
wGenList._windyaw[ nn ] = day._windyaw[ nTime ]
|
|
end
|
|
for nTime, var in pairs( day._weather ) do
|
|
if var.sName == "Clear" then -- Ignore clear weathers. They're default.
|
|
lastWType = "Clear"
|
|
continue
|
|
end
|
|
local nTimeStart = nTime + (i - 1) * 1440
|
|
local nTimeMax = nTimeStart + math.Round(math.random(var.nDuration / 4, var.nDuration / 2), 1)
|
|
local nTimeMaxEnd = nTimeStart + var.nDuration * 0.75
|
|
local nTimeEnd = nTimeStart + var.nDuration
|
|
|
|
local wObj = StormFox2.Weather.Get(var.sName)
|
|
local t = {
|
|
["sName"] = var.sName,
|
|
["fAmount"] = math.Round(var.fAmount, 2),
|
|
["bThunder"] = var.nThunder
|
|
}
|
|
local useCloud = wObj.Inherit == "Cloud" and math.random(1, 10) >= 5
|
|
local startWType = useCloud and "Cloud" or var.sName
|
|
if lastWType == var.sName and lastWTime == nTimeStart then -- In case we had the same weather type before, remove the "fading out" part
|
|
wGenList._weather[ lastWTime ] = nil
|
|
startWType = var.sName
|
|
else
|
|
wGenList._weather[ nTimeStart ] = {
|
|
["sName"] = startWType,
|
|
["fAmount"] = 0
|
|
}
|
|
end
|
|
wGenList._weather[ nTimeMax ] = {
|
|
["sName"] = startWType,
|
|
["fAmount"] = math.Round(var.fAmount, 2)
|
|
}
|
|
wGenList._weather[ nTimeMaxEnd ] = t
|
|
wGenList._weather[ nTimeEnd ] = {
|
|
["sName"] = var.sName,
|
|
["fAmount"] = 0
|
|
}
|
|
lastWType = var.sName
|
|
lastWTime = var.nTimeEnd
|
|
end
|
|
end
|
|
-- Push it into an index
|
|
wGenList.weather = TimeToIndex( wGenList._weather )
|
|
wGenList.temperature = TimeToIndex( wGenList._temperature )
|
|
wGenList.wind = TimeToIndex( wGenList._wind )
|
|
wGenList.windyaw = TimeToIndex( wGenList._windyaw )
|
|
if not IsUsingWeatherGen() then return end -- Don't update the forecast. But keep the weather in mind in case it gets enabled.
|
|
SetForecast( wGenList )
|
|
end
|
|
-- In case settings change, update weekweather
|
|
local function ClearAndRedo()
|
|
timer.Simple(1, function()
|
|
weatherWeekCount = {}
|
|
generator = {}
|
|
weather_index = 0
|
|
wind_index = 0
|
|
temp_index = 0
|
|
-- Generate new Day
|
|
GenerateWeek()
|
|
-- Make WGen
|
|
PushDayToList()
|
|
end)
|
|
end
|
|
min_temp:AddCallback( ClearAndRedo, "weekWeather" )
|
|
max_temp:AddCallback( ClearAndRedo, "weekWeather" )
|
|
max_wind:AddCallback( ClearAndRedo, "weekWeather" )
|
|
night_temp:AddCallback( ClearAndRedo, "weekWeather" )
|
|
OnWeatherSettingChange = ClearAndRedo
|
|
|
|
local lastWeather, lastWind, lastTemp = -1, -1 , -1
|
|
|
|
local function fkey( x, a, b )
|
|
return (x - a) / (b - a)
|
|
end
|
|
|
|
local function findNext( tab, time ) -- First one is time
|
|
for i, v in ipairs( tab ) do
|
|
if time > v[1] then continue end
|
|
return i
|
|
end
|
|
return 0
|
|
end
|
|
|
|
local weather_index = 0
|
|
local wind_index = 0
|
|
local temp_index = 0
|
|
|
|
local function EnableWGenerator(forceCall)
|
|
if enable and not forceCall then return end
|
|
enable = true
|
|
GenerateWeek() -- Generate a week
|
|
PushDayToList() -- Push said list to w-table.
|
|
-- We need to set the start-weather
|
|
-- Set the start temperature
|
|
local curTime = math.ceil(StormFox2.Time.Get())
|
|
local t_index = findNext( wGenList.temperature, curTime )
|
|
if t_index > 0 then
|
|
local procentStart = 1
|
|
local _start = wGenList.temperature[t_index - 1]
|
|
local _end = wGenList.temperature[t_index]
|
|
if _start then
|
|
procentStart = fkey( curTime, _start[1], _end[1] )
|
|
end
|
|
local temp = Lerp( procentStart, (_start or _end)[2], _end[2] )
|
|
StormFox2.Temperature.Set( math.Round(temp, 2), 0 )
|
|
end
|
|
|
|
-- Set the start wind
|
|
local wind_index = findNext( wGenList.wind, curTime )
|
|
if wind_index > 0 then
|
|
local procentStart = 1
|
|
local _start = wGenList.wind[t_index - 1]
|
|
local _end = wGenList.wind[t_index]
|
|
if _start then
|
|
procentStart = fkey( curTime, _start[1], _end[1] )
|
|
end
|
|
local wind = Lerp( procentStart, (_start or _end)[2], _end[2] )
|
|
StormFox2.Wind.SetForce( math.Round(wind, 2), 0 )
|
|
|
|
local _start = wGenList.windyaw[t_index - 1]
|
|
local _end = wGenList.windyaw[t_index]
|
|
local windyaw = Lerp( procentStart, (_start or _end)[2], _end[2] )
|
|
StormFox2.Wind.SetYaw( math.Round(windyaw, 2), 0 )
|
|
end
|
|
|
|
-- Set the start weather
|
|
local weather_index = findNext( wGenList.weather, curTime )
|
|
if weather_index > 0 then
|
|
local procentStart = 1
|
|
local _start = wGenList.weather[weather_index - 1]
|
|
local _end = wGenList.weather[weather_index]
|
|
if _start then
|
|
procentStart = fkey( curTime, _start[1], _end[1] )
|
|
else
|
|
_start = _end
|
|
end
|
|
local isClear = _end[2].sName == "Clear" or _end[2].fAmount == 0
|
|
local w_type = ( isClear and _start[2] or _end[2] ).sName
|
|
local w_procent = ( isClear and _start[2] or _end[2] ).fAmount
|
|
StormFox2.Weather.Set( w_type, w_procent * procentStart )
|
|
if _end[2].bThunder then
|
|
StormFox2.Thunder.SetEnabled(true, _end[2].bThunder)
|
|
else
|
|
StormFox2.Thunder.SetEnabled(false, 0)
|
|
end
|
|
end
|
|
-- Create a timer to modify the weather, checks every 1.5 seconds
|
|
timer.Create("SF_WGEN_DEF", 0.5, 0, function()
|
|
local cT = StormFox2.Time.Get()
|
|
local e_index = findNext( wGenList.weather, cT )
|
|
local i_index = findNext( wGenList.wind, cT )
|
|
local t_index = findNext( wGenList.temperature, cT )
|
|
if weather_index~= e_index then
|
|
weather_index = e_index
|
|
local w_data = wGenList.weather[e_index]
|
|
if w_data then
|
|
local delta = StormFox2.Time.SecondsUntil(w_data[1])
|
|
StormFox2.Weather.Set(w_data[2].sName, w_data[2].fAmount, delta )
|
|
if w_data[2].bThunder then
|
|
StormFox2.Thunder.SetEnabled(true, w_data[2].bThunder)
|
|
else
|
|
StormFox2.Thunder.SetEnabled(false, 0)
|
|
end
|
|
end
|
|
end
|
|
if wind_index~= i_index then
|
|
wind_index = i_index
|
|
local i_data = wGenList.wind[i_index]
|
|
local y_data = wGenList.windyaw[i_index]
|
|
|
|
if i_data then
|
|
local secs = StormFox2.Time.SecondsUntil(i_data[1])
|
|
StormFox2.Wind.SetForce(i_data[2], secs)
|
|
if y_data then
|
|
StormFox2.Wind.SetYaw( y_data[2], secs)
|
|
end
|
|
end
|
|
end
|
|
if temp_index~= t_index then
|
|
temp_index = t_index
|
|
local t_data = wGenList.temperature[t_index]
|
|
if t_data then
|
|
local delta = StormFox2.Time.SecondsUntil(t_data[1])
|
|
StormFox2.Temperature.Set(t_data[2], delta )
|
|
end
|
|
end
|
|
|
|
end)
|
|
end
|
|
local function DisableWGenerator()
|
|
if not enable then return end
|
|
enable = false
|
|
timer.Destroy("SF_WGEN_DEF")
|
|
end
|
|
hook.Add("StormFox2.Time.NextDay", "StormFox2.WGen.ND", function()
|
|
if not enable then return end
|
|
-- Generate new Day
|
|
GenerateDay()
|
|
-- Make WGen
|
|
PushDayToList()
|
|
weather_index = 0
|
|
wind_index = 0
|
|
temp_index = 0
|
|
end)
|
|
-- Logic
|
|
local function NewWGenSetting()
|
|
if not auto_weather:GetValue() then -- Auto weather is off. Make sure API is off too
|
|
DisableAPI()
|
|
DisableWGenerator()
|
|
return
|
|
end
|
|
if API_ENABLE:GetValue() == true then
|
|
EnableAPI()
|
|
DisableWGenerator()
|
|
StormFox2.Msg("Using OpenWeatherMap API")
|
|
else
|
|
DisableAPI()
|
|
EnableWGenerator()
|
|
StormFox2.Msg("Using WeatherGen")
|
|
end
|
|
end
|
|
API_ENABLE:AddCallback( NewWGenSetting, "API_Enable")
|
|
auto_weather:AddCallback( NewWGenSetting, "WGEN_Enable")
|
|
hook.Add("stormfox2.postinit", "WGenInit", NewWGenSetting)
|
|
|
|
|
|
NewWGenSetting()
|
|
hook.Add("StormFox2.data.initspawn", "StormFox2.Weather.SendForcast",function( ply )
|
|
if not hide_forecast then return end
|
|
if not forecast then return end -- ?
|
|
net.Start("StormFox2.weekweather")
|
|
net.WriteBool( forecast.unix_time )
|
|
net.WriteTable( forecast.temperature or {} )
|
|
net.WriteTable( forecast.weather or {} )
|
|
net.WriteTable( forecast.wind or {} )
|
|
net.WriteTable( forecast.windyaw or {} )
|
|
net.Broadcast()
|
|
end) |