The Problem with Desynchronized Weather
By default, GTA V handles weather and time independently on each client. This means one player can be driving through bright sunshine while another player standing right next to them sees a thunderstorm. For roleplay servers, this breaks immersion completely. Imagine trying to coordinate a scene where a character complains about the rain while another player's screen shows clear skies. A synchronized weather and time system ensures every player on the server experiences the same environment at the same moment, which is fundamental for consistent roleplay, realistic driving conditions, and event coordination. In this tutorial, we will build a complete weather synchronization system with configurable time speed, seasonal transitions, weather voting, and blackout events.
Server-Side Weather State Management
The server is the single source of truth for weather and time. It maintains the current weather type, the next weather type for transitions, and the in-game clock. Every client syncs to this state when they join and receives updates whenever the weather changes. GTA V supports a specific set of weather types including CLEAR, EXTRASUNNY, CLOUDS, OVERCAST, RAIN, THUNDER, CLEARING, FOGGY, SMOG, SNOW, SNOWLIGHT, BLIZZARD, and XMAS. Define logical transition rules so the weather flows naturally rather than jumping from blizzard to extra sunny without an intermediate state.
-- server.lua
local Config = {
timeSpeed = 2, -- how many in-game minutes per real second
syncInterval = 30000, -- full sync broadcast every 30s
autoChange = true, -- automatic weather cycling
changeCooldown = 600, -- seconds between automatic changes
}
local State = {
weather = 'CLEAR',
nextWeather = 'CLOUDS',
hour = 12,
minute = 0,
frozen = false,
blackout = false,
season = 'summer',
}
local weatherTransitions = {
EXTRASUNNY = {'CLEAR', 'CLOUDS'},
CLEAR = {'EXTRASUNNY', 'CLOUDS', 'OVERCAST'},
CLOUDS = {'CLEAR', 'OVERCAST', 'RAIN'},
OVERCAST = {'CLOUDS', 'RAIN', 'THUNDER', 'FOGGY'},
RAIN = {'OVERCAST', 'THUNDER', 'CLEARING'},
THUNDER = {'RAIN', 'OVERCAST'},
CLEARING = {'CLEAR', 'CLOUDS'},
FOGGY = {'CLEAR', 'CLOUDS', 'SMOG'},
SMOG = {'FOGGY', 'CLEAR'},
}
local seasonWeights = {
summer = {EXTRASUNNY = 30, CLEAR = 30, CLOUDS = 20, RAIN = 10, THUNDER = 5, FOGGY = 5},
winter = {CLOUDS = 20, OVERCAST = 20, SNOW = 25, SNOWLIGHT = 15, BLIZZARD = 10, FOGGY = 10},
spring = {CLEAR = 25, CLOUDS = 25, RAIN = 20, CLEARING = 15, FOGGY = 10, OVERCAST = 5},
autumn = {CLOUDS = 25, OVERCAST = 25, RAIN = 20, FOGGY = 15, CLEAR = 10, SMOG = 5},
}
Time Progression and Synchronization Loop
The time system runs as a server-side loop that increments the in-game clock based on the configured speed. A timeSpeed of 2 means every real-world second advances the in-game clock by 2 minutes. This creates a day-night cycle that takes approximately 12 real-world minutes for a full 24 in-game hours. The server broadcasts the current time and weather state to all clients at regular intervals and also pushes immediate updates when an admin changes the weather or triggers a blackout event. New players joining mid-session receive the current state through a dedicated sync event so they immediately match everyone else.
-- Time progression loop (server.lua continued)
CreateThread(function()
while true do
Wait(1000)
if not State.frozen then
State.minute = State.minute + Config.timeSpeed
if State.minute >= 60 then
State.hour = State.hour + math.floor(State.minute / 60)
State.minute = State.minute % 60
end
if State.hour >= 24 then
State.hour = State.hour % 24
end
end
end
end)
-- Periodic full sync
CreateThread(function()
while true do
Wait(Config.syncInterval)
TriggerClientEvent('weather:fullSync', -1, State)
end
end)
-- Sync new players on join
AddEventHandler('playerJoining', function()
local src = source
Wait(2000)
TriggerClientEvent('weather:fullSync', src, State)
end)
Client-Side Weather Application
On the client, you need to override GTA's native weather and time systems. The key natives are SetWeatherTypeNowPersist for instant weather changes, SetWeatherTypeTransition for smooth blending between two weather states, and NetworkOverrideClockTime for setting the in-game clock. You must call NetworkOverrideClockTime every frame to prevent the game from reverting to its own time calculation. For weather transitions, GTA supports a blend factor between 0.0 and 1.0 that lets you smoothly interpolate between two weather states over several seconds, creating realistic transitions where clouds gradually roll in before rain starts.
-- client.lua
local currentWeather = 'CLEAR'
local nextWeather = 'CLEAR'
local weatherBlend = 0.0
local hour, minute = 12, 0
local blackout = false
RegisterNetEvent('weather:fullSync', function(state)
currentWeather = state.weather
nextWeather = state.nextWeather or state.weather
hour = state.hour
minute = state.minute
blackout = state.blackout or false
applyWeather()
end)
function applyWeather()
ClearOverrideWeather()
ClearWeatherTypePersist()
SetWeatherTypeNowPersist(currentWeather)
if currentWeather ~= nextWeather then
SetWeatherTypeTransition(
GetHashKey(currentWeather),
GetHashKey(nextWeather),
weatherBlend
)
end
if blackout then
SetArtificialLightsState(true)
SetArtificialLightsStateAffectsVehicles(false)
else
SetArtificialLightsState(false)
end
end
-- Override clock every frame
CreateThread(function()
while true do
Wait(0)
NetworkOverrideClockTime(hour, minute, 0)
end
end)
Weather Voting System
A weather voting system lets your community decide the weather democratically. Players can cast a vote for their preferred weather type, and after a voting period the most popular option wins. This works well for events where you want player engagement, like deciding whether a car meet happens under clear skies or dramatic thunderstorms. Limit voting to once per cycle to prevent spam, and display the current vote counts through your notification system or a simple NUI overlay. The server collects votes, tallies them when the period expires, and applies the winning weather through a smooth transition so all players experience the change simultaneously.
-- Weather voting (server.lua)
local votes = {}
local voteCooldown = {}
local votingOpen = false
local voteOptions = {'CLEAR', 'RAIN', 'THUNDER', 'FOGGY', 'SNOW'}
RegisterCommand('voteweather', function(source, args)
if not votingOpen then
TriggerClientEvent('chat:addMessage', source, {args = {'Weather', 'No vote is currently active.'}})
return
end
if voteCooldown[source] then
TriggerClientEvent('chat:addMessage', source, {args = {'Weather', 'You already voted this round.'}})
return
end
local choice = string.upper(args[1] or '')
local valid = false
for _, opt in ipairs(voteOptions) do
if opt == choice then valid = true break end
end
if not valid then
TriggerClientEvent('chat:addMessage', source, {
args = {'Weather', 'Options: ' .. table.concat(voteOptions, ', ')}
})
return
end
votes[choice] = (votes[choice] or 0) + 1
voteCooldown[source] = true
TriggerClientEvent('chat:addMessage', -1, {
args = {'Weather', GetPlayerName(source) .. ' voted for ' .. choice}
})
end, false)
function startWeatherVote()
votes = {}
voteCooldown = {}
votingOpen = true
TriggerClientEvent('chat:addMessage', -1, {
args = {'Weather', 'Weather vote started! Use /voteweather [type]. Options: ' .. table.concat(voteOptions, ', ')}
})
SetTimeout(60000, function()
votingOpen = false
local winner, maxVotes = 'CLEAR', 0
for weather, count in pairs(votes) do
if count > maxVotes then winner = weather; maxVotes = count end
end
transitionWeather(winner)
TriggerClientEvent('chat:addMessage', -1, {
args = {'Weather', 'Vote ended! Weather changing to: ' .. winner}
})
end)
end
Seasonal System and Weather Effects on Gameplay
A seasonal system adds a layer of depth that most servers overlook. By tracking the in-game calendar or mapping real-world months to seasons, you can weight weather probabilities accordingly. Summer favors clear and sunny weather with occasional thunderstorms, while winter shifts the balance toward snow, overcast skies, and fog. Seasons can also affect gameplay mechanics beyond just visuals. During rain, you could reduce vehicle traction by triggering a handling modifier on all active vehicles. Fog could reduce the render distance for AI peds, making stealth gameplay more viable. Snow could slow player movement speed slightly and require winter clothing to avoid a slow health drain. These small touches create an immersive world that feels alive and responsive to its environment.
Blackout Events and Admin Controls
Blackout events are a powerful tool for server events and roleplay scenarios. The SetArtificialLightsState native disables all artificial light sources in the game world, plunging the city into darkness. Combined with an overcast or foggy weather state, this creates an incredibly atmospheric environment perfect for horror events, heist scenarios, or survival roleplay. Admin commands should provide full control over the weather system: freezing time, setting specific hours, forcing weather types, triggering blackouts, and starting weather votes. Protect these commands with ace permissions so only authorized staff can modify the environment. A well-designed weather system becomes a storytelling tool that admins use to set the mood for every event and scenario on your server.
-- Admin commands (server.lua)
RegisterCommand('setweather', function(source, args)
if source > 0 and not IsPlayerAceAllowed(source, 'command.setweather') then return end
local weather = string.upper(args[1] or 'CLEAR')
transitionWeather(weather)
TriggerClientEvent('chat:addMessage', -1, {
args = {'Admin', 'Weather changed to ' .. weather}
})
end, true)
RegisterCommand('settime', function(source, args)
if source > 0 and not IsPlayerAceAllowed(source, 'command.settime') then return end
State.hour = tonumber(args[1]) or 12
State.minute = tonumber(args[2]) or 0
TriggerClientEvent('weather:fullSync', -1, State)
end, true)
RegisterCommand('blackout', function(source, args)
if source > 0 and not IsPlayerAceAllowed(source, 'command.blackout') then return end
State.blackout = not State.blackout
TriggerClientEvent('weather:fullSync', -1, State)
TriggerClientEvent('chat:addMessage', -1, {
args = {'Admin', 'Blackout ' .. (State.blackout and 'enabled' or 'disabled')}
})
end, true)
RegisterCommand('freezetime', function(source, args)
if source > 0 and not IsPlayerAceAllowed(source, 'command.freezetime') then return end
State.frozen = not State.frozen
TriggerClientEvent('chat:addMessage', -1, {
args = {'Admin', 'Time ' .. (State.frozen and 'frozen' or 'unfrozen')}
})
end, true)