Tutorial 2026-03-11

FiveM Weather Synchronization & Dynamic Time System

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

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)

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.