Browse docs

Weather integrations

Configure local weather and time in Sky Base, prepare QB WeatherSync or CD EasyTime, and migrate Activities settings.

Use this tutorial when setting up local activity or interior lighting through Sky Base. Complete the common setup, then follow the section for your weather resource. The QB and CD sections contain the required source changes; no patch files are installed with Sky Base.

Select the provider

In the existing Config = { ... } table in sky_base/config/config.lua, set:

weather = "auto",

Start your chosen weather resource before sky_base, followed by sky_jobs_base and sky_activities. For example, after your framework and the provider setup below:

ensure qb-weathersync
ensure sky_base
ensure sky_jobs_base
ensure sky_activities

Config.weather = "auto" selects the first started resource in this order: cd_easytime, Renewed-Weathersync, esx_weather, qb-weathersync, weathersync. Without a match it uses native. Codesign and Renewed precede their QB compatibility name. Run one actual synchronization provider. Explicit selections use the resource name, native, or custom. Like fuel and vehiclekeys, auto-detection runs once in source/shared/set_config.lua. Each adapter uses if Sky.Config.weather == "..." then and defines its Sky.Functions integration functions only when selected. Start the provider before Base; restart Base to select a different integration. An explicitly unavailable provider fails visibly.

Use the local override

This client API never changes global server weather/time:

local success, reason = Sky.Functions.SetWeatherOverride("my-interior", "CLEAR", 12)
Sky.Functions.SetWeatherOverride("my-interior", "CLEAR", 0)
Sky.Functions.ClearWeatherOverride("my-interior")
local current = Sky.Functions.GetWeatherOverride()

SetWeatherOverride(scope, weather, hour) freezes an integer hour 0–23, with minutes/seconds zero. Weather names are the standard GTA types listed in source/client/weather.lua. It returns true or false, reason. Updating the same scope preserves its original restoration snapshot. ClearWeatherOverride(scope) has the same result shape and is idempotent when no scope exists. GetWeatherOverride() returns false or {scope, owner, weather, hour, provider, active}. One resource/scope owns the environment; another receives override_in_use and cannot clear it. Cfx invocation context identifies the owner. Base releases automatically when that owner or Base stops.

Weather now uses Sky.Functions, like fuel. Replace former Sky.Weather calls with the three functions above. Custom provider tables are replaced by the direct integration functions described below. Update Base and its consumers together.

The selected provider's restart rebinds outstanding requests, including unavailable ones. A stopping provider is not called. Renewed's initial clock application runs after its state handlers, and is cancelled on release. Stable targets do not repeat native writes.

Prepare your weather resource

The following changes address writers that continue after a pause request. They were checked against the linked upstream revisions. Compare your installed files first and preserve your own customizations. Only modify the provider you use.

Each block is a unified diff: remove lines starting with -, add lines starting with +, and keep the surrounding context. Do not paste the diff markers or @@ headings into Lua. The sky_weather_pause '1' manifest entry belongs to the weather resource, not Sky Base. Add it only after applying all corresponding client changes: Base checks this entry before using the provider.

QB WeatherSync

  1. Open qb-weathersync/client.lua and apply the two loop checks below. Rechecking the disabled state after each wait prevents an already running transition from writing into the local override.
  2. Add the shown metadata to qb-weathersync/fxmanifest.lua.
  3. Restart qb-weathersync, then Sky Base and its consumers in the configured start order.

Source: qbcore-fivem/qb-weathersync at f7ae442137ff0cd2e62fee75e2f963465c5429be.

--- a/client.lua
+++ b/client.lua
@@ -45,6 +45,7 @@
                 Wait(15000)
             end
             Wait(100) -- Wait 0 seconds to prevent crashing.
+            if disable then goto continue end
             SetArtificialLightsState(blackout)
             SetArtificialLightsStateAffectsVehicles(blackoutVehicle)
             ClearOverrideWeather()
@@ -69,6 +70,7 @@
         else
             Wait(1000)
         end
+        ::continue::
     end
 end)

@@ -82,6 +84,7 @@
     while true do
         if not disable then
             Wait(0)
+            if disable then goto continue end
             local _, _, _, hours, minutes, _ = GetLocalTime()
             local newBaseTime = baseTime
             if tick - (Config.RealTimeSync and 500 or 22) > tick then
@@ -107,5 +110,6 @@
         else
             Wait(1000)
         end
+        ::continue::
     end
 end)
--- a/fxmanifest.lua
+++ b/fxmanifest.lua
@@ -14,3 +14,6 @@

 server_script 'server.lua'
 client_script 'client.lua'
+
+-- Local pause fixes required by Sky.Functions weather overrides (see the weather integration tutorial).
+sky_weather_pause '1'

CD EasyTime

  1. Open cd_easytime/client/client.lua and apply all changes below. They update the local target during a pause, cancel old pause/release coroutines and suspend clock/rain writers, including a time transition already in progress.
  2. Add the shown metadata to cd_easytime/fxmanifest.lua.
  3. Restart cd_easytime, then Sky Base and its consumers in the configured start order.

The optional third argument of cd_easytime:PauseSync(boolean, hours, weather) is added by these changes. Existing callers retain their default hour/weather. Release requests the full state through cd_easytime:SyncMe so an already frozen server clock resumes its freeze loop. Base rejects an already paused CD provider because the existing getter does not expose its previous pause target.

Source: dsheedes/cd_easytime at 174ddc1e823d1aad54361988ea54c302c08b980a. Codesign documents the pause-state getter in its exports reference.

--- a/client/client.lua
+++ b/client/client.lua
@@ -6,6 +6,7 @@
 local my_source = GetPlayerServerId(PlayerId())
 PauseSync = {}
 PauseSync.state = false
+local pause_generation = 0


 RegisterNetEvent('cd_easytime:OpenUI', function(values)
@@ -17,22 +18,29 @@
     SendNUIMessage({action = 'open', values = values})
 end)

-RegisterNetEvent('cd_easytime:PauseSync', function(boolean, hours)
-    if boolean == PauseSync.state then return end
+RegisterNetEvent('cd_easytime:PauseSync', function(boolean, hours, weather)
     if boolean then
+        PauseSync.hours = hours or 20
+        ChangeWeather(weather or 'EXTRASUNNY', true)
+        if PauseSync.state then return end
         PauseSync.state = true
-        ChangeWeather('EXTRASUNNY', true)
-        while PauseSync.state do
+        pause_generation = pause_generation + 1
+        local generation = pause_generation
+        while PauseSync.state and generation == pause_generation do
+            NetworkOverrideClockTime(PauseSync.hours, 0, 0)
             Wait(0)
-            NetworkOverrideClockTime(hours or 20, 00, 00)
-        end
-    else
+        end
+    else
+        if not PauseSync.state then return end
         PauseSync.state = false
+        pause_generation = pause_generation + 1
+        local generation = pause_generation
         Wait(300)
-        TriggerServerEvent('cd_easytime:SyncMe_basics', {weather = true, time = true})
-    end
-end)
-
+        if not PauseSync.state and generation == pause_generation then
+            TriggerServerEvent('cd_easytime:SyncMe')
+        end
+    end
+end)
 RegisterNetEvent('cd_easytime:ForceUpdate', function(data, source)
     if not PauseSync.state then
         if data.weather ~= nil and data.weather ~= self.weather then
@@ -64,6 +72,7 @@
     CreateThread(function()
         while self.freeze and not PauseSync.state do
             Wait(0)
+            if PauseSync.state then break end
             NetworkOverrideClockTime(data.hours, data.mins, 0)
         end
     end)
@@ -113,7 +122,7 @@
     CreateThread(function()
         while true do
             Wait(5) --increase this timer to reduce resource usage. but this may cause jumping clouds.
-            if not time_transition and not self.freeze and not forcedInstantTimeUpdate then
+            if not PauseSync.state and not time_transition and not self.freeze and not forcedInstantTimeUpdate then
                 NetworkOverrideClockTime(self.hours, self.mins, 0)
             end
         end
@@ -146,6 +155,7 @@

     local start_time = GetGameTimer()
     while GetGameTimer() - start_time < (transition_speed*1000) do
+        if PauseSync.state then return end
         local progress = (GetGameTimer() - start_time) / (transition_speed*1000)
         local total_seconds = (current_total_seconds + ((target_total_seconds - current_total_seconds) * progress)) % 86400

@@ -157,6 +167,7 @@
         if forcedInstantTimeUpdate then break end
         Wait(0)
     end
+    if PauseSync.state then return end
     if forcedInstantTimeUpdate then NetworkOverrideClockTime(self.hours, self.mins, 0) return end
     NetworkOverrideClockTime(data.hours, data.mins, data.seconds or 0)
 end
@@ -263,7 +274,7 @@
 CreateThread(function()
     while true do
         Wait(2000)
-        if self.weather then
+        if self.weather and not PauseSync.state then
             if RainWeathers[self.weather] then
                 SetRainLevel(1.0)
             else
--- a/fxmanifest.lua
+++ b/fxmanifest.lua
@@ -61,4 +61,6 @@

 provide 'vSync'
 provide 'qb-weathersync'
-provide 'qbx_weathersync'
\ No newline at end of file
+provide 'qbx_weathersync'
+-- Local pause fixes required by Sky.Functions weather overrides (see the weather integration tutorial).
+sky_weather_pause '1'

Check the setup

Enter the Activities center, start and leave a night activity, return to the center, and leave it. Check noon in the center, midnight during the night activity, clear local weather and normal server synchronization after leaving. Repeat during a weather transition and with server time already frozen. Another player outside Activities should retain ordinary server weather/time.

If the console asks you to complete this tutorial, verify both the client edits and the metadata in the selected provider. Changing the metadata alone does not fix its writers. Review these changes again when updating that provider.

Existing adapters and restoration

  • ESX pauses via ToggleWeatherSync / ToggleTimeSync. A removable native weather layer preserves the persistent baseline. ESX does not reapply unchanged-zone time on resume, so Base restores the captured clock plus elapsed game time. Administrator changes during the override are not represented in that snapshot.
  • Kibook uses setMyWeather / setMyTime; release enables sync and requests weathersync:init for current state.
  • Renewed preserves syncWeather / playerWeather, changes them without replication, and restores them plus the captured clock rate on release.
  • Native uses removable weather/clock overrides and elapsed-clock restoration. There is no authoritative server clock when no provider is present.

ESX, Kibook and QB lack prior-pause getters: these adapters assume normal sync on entry and enable it on release. Unrelated scripts must not independently own the same clock or weather. Native clock pause has no verified getter; Base unpauses on release.

Customer adapters belong in sky_base/config/integration/weather/custom.lua and define these client-side functions directly, like the fuel adapters:

  • Sky.Functions.AcquireWeatherOverride(weather, hour) captures the clock with Sky.Functions.CaptureWeatherState(), pauses local provider sync and returns that state. Store the provider's restoration token in state.token. Return false, reason before changing provider state when acquisition fails. Set state.targets = true if the provider handles both weather and time; set state.deferred = true if its initial apply must wait for the provider's state handlers.
  • Sky.Functions.ApplyWeatherOverride(weather, hour, state) updates the target while retaining the same restoration state. Call Sky.Functions.ApplyNativeWeatherOverride(weather, hour) for removable native weather/time overrides, or apply both targets through the provider and call PauseClock(true).
  • Sky.Functions.ReleaseWeatherOverride(state) calls Sky.Functions.RestoreWeatherState(state) before restoring provider sync from state.token. The shared helper clears native overrides and restores the elapsed clock when state.targets is absent, and always unpauses the clock. Base uses RestoreWeatherState(state, true) for a stopped provider without calling its unavailable exports.

Acquire/apply must not yield. The manifest loads native.lua first so its shared helpers are available to every adapter. Native-only integrations reuse those functions directly; the custom template fails explicitly until implemented. Consumer resources continue to use SetWeatherOverride, ClearWeatherOverride and GetWeatherOverride, which own validation, scope ownership and resource cleanup.

Activities migration and editor ownership

Move Activities' old Config.Environment.provider value to Base's Config.weather. Move customer ActivityEnvironmentProviders.custom callbacks to the direct Sky.Functions.AcquireWeatherOverride, ApplyWeatherOverride and ReleaseWeatherOverride functions in sky_base/config/integration/weather/custom.lua. Acquisition now returns the captured clock state with the former provider token in state.token; apply receives (weather, hour, state) and release receives that same state. Activities no longer loads config/environment.lua. Update Base and Activities together. Environment.enabled remains in /activitiesconfig with unchanged persistence/defaults.

Job Configurator impact: provider selection/callbacks are plaintext-only bootstrap integration settings. Base has no /jobconfig registration; Activities keeps its existing feature switch in /activitiesconfig. No persisted setting key or SQL migration changes.

Source and test boundary

Cfx revision e34d12cd9a39cc223548a5be1ab09f60e9183051 inspected: ext/natives/natives_stash/gta_universal.lua, ext/natives/codegen_out_lua.lua, code/components/citizen-scripting-lua/src/LuaScriptNatives.cpp (LuaScriptNativeContext::PushArgument, Lua_DoInvokeNative), code/components/scripting-gta/src/ScriptEngine.cpp (CallNativeHandlerRage), code/components/extra-natives-five/src/TimeExtraNatives.cpp (SetMillisecondsPerGameMinute), code/components/citizen-scripting-core/src/ResourceScriptFunctions.cpp, and code/components/citizen-scripting-core/src/MetadataScriptFunctions.cpp (metadata lookup), code/components/citizen-scripting-core/src/ResourceScriptingComponent.cpp (CreateEnvironments loads shared scripts before the ordered client script list), data/shared/citizen/scripting/lua/scheduler.lua (event/export dispatch and state bags). Official client weather/clock signatures and shared resource lookup/metadata/invocation signatures were checked. Lua booleans marshal as 0/1, clock arguments as integers, rain as a float; these calls have no pointer outputs. Native overrides are client-local, with no server RPC. Renewed uses explicit state:set(..., false).

For the Sky.Functions migration, ResourceScriptFunctions.cpp (GET_INVOKING_RESOURCE, GET_CURRENT_RESOURCE_NAME) and scheduler.lua (funcref_mt packing/invocation) were rechecked at that revision. The functions remain in Base's runtime and retain the caller's resource context after the namespace change. The existing environment contracts pass for all six providers using the manifest's actual adapter load order. The local ESX server is reachable, but no client is connected for a live weather/rendering check.

Observed server: FXServer 36897. The cached Cfx revision is not proven to match that artifact or the connected client. GTA weather/clock internals are proprietary; source and mocked contracts do not prove engine rendering. Activities contracts exercise all six adapters and lifecycle/ownership. Actual upstream coroutine reproductions fail before and pass after the provider changes shown above. No performance claim or profiler measurement is made.