Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b5d3b6c
init
Kenshiin13 Oct 21, 2025
d96172c
refactor: avoid ipairs
Kenshiin13 Oct 21, 2025
cabee08
feat: show current zone in nui
Kenshiin13 Oct 21, 2025
2b5f4e7
...
Kenshiin13 Oct 21, 2025
5592c65
fix(esx_weather): add weathertypes to nui
Kenshiin13 Oct 22, 2025
1359efa
refactor(esx_weather): load weathertypes once
Kenshiin13 Oct 22, 2025
2cb57c9
feat(esx_weather): add export to toggle sync
Kenshiin13 Oct 22, 2025
43ede58
refactor(esx_weather): refactor to modular structure
Kenshiin13 Oct 22, 2025
cfb5f8a
refactor(esx_weather): change event name
Kenshiin13 Oct 22, 2025
9365f59
feat: add time sync
Kenshiin13 Oct 24, 2025
10ccf9d
refactor: use 1 loop
Kenshiin13 Oct 25, 2025
128bd49
refactor: use guard clauses
Kenshiin13 Oct 25, 2025
815d4c1
refactor: avoid useless obj creation
Kenshiin13 Oct 25, 2025
9b88cc7
refactor: add cfg type annotations
Kenshiin13 Oct 25, 2025
492c050
refactor: add zone enum
Kenshiin13 Oct 25, 2025
e7b1739
feat: add fxv2 oal
Kenshiin13 Oct 25, 2025
ba7aac5
chore: update zone time offsets to be more reasonable
Kenshiin13 Oct 25, 2025
93d41af
...
Kenshiin13 Oct 25, 2025
7b65b6f
chore: disable debug by default
Kenshiin13 Oct 25, 2025
b173627
feat: add ToggleTimeSync export
Kenshiin13 Oct 25, 2025
9703158
..
Kenshiin13 Oct 25, 2025
e0c4d30
feat: add esx_weather ui
simpleC0de Oct 25, 2025
d317e9f
feat: add modern ui for esx_weather
simpleC0de Oct 25, 2025
8246f06
fix: remove unnecessary file
simpleC0de Oct 25, 2025
cae61e5
Merge pull request #92 from simpleC0de/esx_weather
Kr3mu Oct 26, 2025
73dfc74
refactor: Use `zoneName` instead of `zone`
Zykem Oct 30, 2025
9b9286b
fix: Apply MS per Game Minute & race condition fix
Zykem May 28, 2026
4009140
feat: Add time management events & functions
Zykem May 28, 2026
4293fbe
fix: return SerializedTime in add function as expected
Zykem May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions [esx_addons]/esx_weather/client/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---@param WeatherByZone table<Zone, WeatherType>
RegisterNetEvent("esx_weather:client:weather:setZones", function(WeatherByZone)
Modules.Weather.ByZone = WeatherByZone
Modules.NUI.updateWeatherZones(Modules.Weather.ByZone)
Shared.Modules.Debug.print("Received weather zones from server:", json.encode(Modules.Weather.ByZone, { indent = true }))
end)

---@param zone Zone
---@param weatherType WeatherType
RegisterNetEvent("esx_weather:client:weather:setZone", function(zone, weatherType)
if (not Modules.Weather.ByZone) then return end

local oldWeatherType = Modules.Weather.ByZone[zone]
Modules.Weather.ByZone[zone] = weatherType
Modules.NUI.updateWeatherZones(Modules.Weather.ByZone)
Shared.Modules.Debug.print(("Updated zone %s. Changing weather: %s -> %s"):format(zone, oldWeatherType, weatherType))
end)

---@param currentTime SerializedTime
RegisterNetEvent("esx_weather:client:time:setTime", function(currentTime)
Modules.Time.set(currentTime)
end)

Citizen.CreateThread(function()
while (not ESX.PlayerLoaded) do Citizen.Wait(0) end
while (not Modules.Weather.ByZone) do Citizen.Wait(0) end

SetMillisecondsPerGameMinute(Config.Time.secondsPerGameMinute * 1000)

while (true) do
Modules.Time.tick()
Modules.Weather.tick()
Citizen.Wait(1000)
end
end)

RegisterCommand("weathersync", function()
if (not Config.AdminGroups[ESX.PlayerData.group]) then
return
end

if (not Modules.Weather.ByZone or not Modules.Weather.currentType) then return end

Modules.NUI.show(Modules.Zone.getClosest(), Modules.Weather.currentType, Modules.Weather.ByZone)
end)

exports("ToggleWeatherSync", Modules.Weather.toggleSync)
exports("ToggleTimeSync", Modules.Time.toggleSync)
59 changes: 59 additions & 0 deletions [esx_addons]/esx_weather/client/modules/nui/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Modules = Modules or {}
Modules.NUI = Modules.NUI or {}
Modules.NUI.isOpen = false

---@param currentZone Zone
---@param currentWeather WeatherType
---@param WeatherByZone table<Zone, WeatherType>
function Modules.NUI.show(currentZone, currentWeather, WeatherByZone)
SendNUIMessage({
action = "show",
Data = {
currentZone = currentZone,
currentWeather = currentWeather,
WeatherByZone = WeatherByZone,
}
})
SetNuiFocus(true, true)
Modules.NUI.isOpen = true
end

---@param WeatherByZone table<Zone, WeatherType>
function Modules.NUI.updateWeatherZones(WeatherByZone)
if (not Modules.NUI.isOpen) then return end

SendNUIMessage({
action = "updateWeatherZones",
Data = {
WeatherByZone = WeatherByZone,
}
})
end

---@param Data {zoneName: Zone, weatherType: WeatherType}
---@param cb fun(success:boolean)
RegisterNUICallback("setZoneWeather", function(Data, cb)
TriggerServerEvent("esx_weather:server:setZoneWeather", Data.zoneName, Data.weatherType)
cb(true)
end)

---@param Data {zoneName: Zone, time: Time}
---@param cb fun(success:boolean)
RegisterNUICallback("setZoneTime", function(Data, cb)
TriggerServerEvent("esx_weather:server:setZoneTime", Data.zoneName)
cb(true)
end)

---@param _ nil
---@param cb fun(success:boolean)
RegisterNUICallback("close", function(_, cb)
SetNuiFocus(false, false)
Modules.NUI.isOpen = false
cb(true)
end)

---@param _ nil
---@param cb fun(Data: {WeatherTypes: table<WeatherType, string>})
RegisterNUICallback("ready", function(_, cb)
cb({ WeatherTypes = Shared.Enum.WeatherType })
end)
59 changes: 59 additions & 0 deletions [esx_addons]/esx_weather/client/modules/time/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Modules = Modules or {}
Modules.Time = Modules.Time or {}

Modules.Time.currentTime = false ---@type Time|false
Modules.Time.syncedAt = false ---@type integer|false
Modules.Time.currentZone = false ---@type Zone|false
Modules.Time.isSyncEnabled = true

local MS_PER_SECOND <const> = 1000

---@param currentTime SerializedTime
function Modules.Time.set(currentTime)
Modules.Time.currentTime = currentTime
Modules.Time.syncedAt = GetGameTimer()
end

function Modules.Time.tick()
if (not Modules.Time.isSyncEnabled) then
return
end

if (not Modules.Time.currentTime or not Modules.Time.syncedAt) then
return
end
local closestZone = Modules.Zone.getClosest()

if (Modules.Time.currentZone == closestZone) then
return
end

Modules.Time.currentZone = closestZone
local zoneHoursOffset = Config.Time.Zones[Modules.Time.currentZone] or 0

local elapsedMs = GetGameTimer() - Modules.Time.syncedAt
local elapsedSeconds = math.floor(elapsedMs / MS_PER_SECOND)

local currentTime = Shared.Class.Time:new(Modules.Time.currentTime):add({
hours = zoneHoursOffset,
minutes = 0,
seconds = elapsedSeconds
})

Shared.Modules.Debug.print(("Setting time for zone %s (offset %dh): %02d:%02d:%02d"):format(
Modules.Time.currentZone,
zoneHoursOffset,
currentTime.hours,
currentTime.minutes,
currentTime.seconds
))

AdvanceClockTimeTo(currentTime.hours, currentTime.minutes, currentTime.seconds)
end

---@param toggle boolean
function Modules.Time.toggleSync(toggle)
assert(type(toggle) == "boolean", "toggle must be a boolean")

Modules.Time.isSyncEnabled = toggle
end
28 changes: 28 additions & 0 deletions [esx_addons]/esx_weather/client/modules/weather/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Modules = Modules or {}
Modules.Weather = Modules.Weather or {}
Modules.Weather.ByZone = false ---@type table<Zone, WeatherType> | false
Modules.Weather.currentType = false ---@type WeatherType | false
Modules.Weather.isSyncEnabled = true

---@param toggle boolean
function Modules.Weather.toggleSync(toggle)
assert(type(toggle) == "boolean", "toggle must be a boolean")

Modules.Weather.isSyncEnabled = toggle
end

function Modules.Weather.tick()
if (not Modules.Weather.isSyncEnabled) then
return
end

local currentZone = Modules.Zone.getClosest()
local zoneWeather = Modules.Weather.ByZone[currentZone]
if (not zoneWeather or zoneWeather == Modules.Weather.currentType) then
return
end

Shared.Modules.Debug.print(("Entered zone %s. Changing weather: %s -> %s"):format(currentZone, Modules.Weather.currentType or "NONE", zoneWeather))
Modules.Weather.currentType = zoneWeather
SetWeatherTypeOvertimePersist(zoneWeather, Config.Weather.transitionTimeSeconds * 1.0)
end
18 changes: 18 additions & 0 deletions [esx_addons]/esx_weather/client/modules/zone/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Modules = Modules or {}
Modules.Zone = Modules.Zone or {}

---@return Zone
function Modules.Zone.getClosest()
local playerCoords = GetEntityCoords(ESX.PlayerData.ped).xy

local closestZone, closestZoneDistance = nil, nil
for zone, zoneCoords in pairs(Config.Zones) do
local currentDistance = #(playerCoords - zoneCoords)
if (not closestZone or currentDistance < closestZoneDistance) then
closestZone = zone
closestZoneDistance = currentDistance
end
end

return closestZone
end
51 changes: 51 additions & 0 deletions [esx_addons]/esx_weather/config/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---@class WeatherConfig
---@field ValidTypes WeatherType[]
---@field transitionTimeSeconds integer
---@field cycleTimeSeconds integer

---@class TimeConfig
---@field secondsPerGameMinute integer
---@field Zones table<Zone, integer>

---@class Config
---@field debug boolean
---@field AdminGroups table<string, boolean>
---@field Zones table<Zone, vector2>
---@field Weather WeatherConfig
---@field Time TimeConfig

Config = {
debug = false,
AdminGroups = {
admin = true,
moderator = true,
},
Zones = {
[Shared.Enum.Zone.LOS_SANTOS] = vector2(-33.6997, -1102.0000),
[Shared.Enum.Zone.SANDY_SHORES] = vector2(1728.2000, 3709.3000),
[Shared.Enum.Zone.PALETO_BAY] = vector2(-220.2753, 6408.9116),
},

Weather = {
ValidTypes = {
Shared.Enum.WeatherType.CLEAR,
Shared.Enum.WeatherType.CLOUDS,
Shared.Enum.WeatherType.EXTRASUNNY,
Shared.Enum.WeatherType.FOGGY,
Shared.Enum.WeatherType.OVERCAST,
Shared.Enum.WeatherType.RAIN,
Shared.Enum.WeatherType.SMOG,
Shared.Enum.WeatherType.THUNDER,
},
transitionTimeSeconds = 30,
cycleTimeSeconds = 60 * 30 -- 30 minutes
},
Time = {
secondsPerGameMinute = 15,
Zones = {
[Shared.Enum.Zone.LOS_SANTOS] = 0,
[Shared.Enum.Zone.SANDY_SHORES] = 1,
[Shared.Enum.Zone.PALETO_BAY] = 2,
}
}
} --[[@as Config]]
45 changes: 45 additions & 0 deletions [esx_addons]/esx_weather/fxmanifest.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
fx_version "cerulean"
game "gta5"
author "Kenshin13"
description "Weather sync for your ESX server"
version "1.0.0"
legacyversion "1.13.4"
lua54 "yes"
use_fxv2_oal "yes"

shared_scripts {
"@es_extended/imports.lua",
"/shared/**",
"config/main.lua",
}

client_scripts {
"/client/modules/**",
"client/main.lua",
}

ui_page "ui/index.html"

files {
-- Keep this order.
"ui/index.html",
"ui/style.css",
"ui/css/*.css",
"ui/js/core/state.js",
"ui/js/core/constants.js",
"ui/js/utils/sanitizer.js",
"ui/js/utils/helpers.js",
"ui/js/services/nui-bridge.js",
"ui/js/controllers/ui-controller.js",
"ui/js/handlers/event-handlers.js",
"ui/js/main.js",
}

server_scripts {
"/server/modules/**",
"server/main.lua",
}

dependencies {
"es_extended",
}
28 changes: 28 additions & 0 deletions [esx_addons]/esx_weather/server/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---@param zone Zone
---@param weatherType WeatherType
RegisterNetEvent("esx_weather:server:setZoneWeather", function(zone, weatherType)
local src = source --[[@as integer]]
local xPlayer = ESX.Player(src)
if (not xPlayer) then return end

if (not Config.AdminGroups[xPlayer.getGroup()]) then
return
end

if (not Modules.Weather.ByZone[zone]) then
return
end

Modules.Weather.setZone(zone, weatherType)
end)

---@param src integer
AddEventHandler("esx:playerLoaded", function(src)
Modules.Weather.broadcastZones(src)
Modules.Time.broadcast(src)
end)

Citizen.SetTimeout(1000, function()
Modules.Weather.broadcastZones()
Modules.Time.broadcast()
end)
24 changes: 24 additions & 0 deletions [esx_addons]/esx_weather/server/modules/time/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Modules = Modules or {}
Modules.Time = {}

---@param src integer?
---@param time Time?
function Modules.Time.broadcast(src, time)
TriggerClientEvent("esx_weather:client:time:setTime", (src or -1), time or Modules.Time.get())
end

---@return SerializedTime
function Modules.Time.get()
local t = os.date("*t")

return {
hours = tonumber(t.hour) or 0,
minutes = tonumber(t.min) or 0,
seconds = tonumber(t.sec) or 0
} --[[@as SerializedTime]]
end

---@param time Time
function Modules.Time.setTime(time)
Modules.Time.broadcast(nil, time)
end
Loading