Server Weather API
Weather hook signatures and the weather region object — sampling locations, reading the current weather, and writing rate-independent weather effects.
Weather types declared in weather-config.md can attach server-side Lua hooks. They receive the world object (see lua-server-world.md) and a weather region object, described below.
Hooks
Declared under the weather entry’s scripts: map in your weather config. Each value is a bare path to a .lua file that returns its handler function:
scripts:
server-update: /weathers/snowy/scripts/server_update.lua
server-on-start: /weathers/snowy/scripts/server_on_start.lua
server-on-end: /weathers/snowy/scripts/server_on_end.lua
scripts: key | When Called | Signature |
|---|---|---|
server-update | Every server tick while this weather is active | function(world, weather_region, delta_ns) |
server-on-start | When this weather type begins in a region | function(world, weather_region) |
server-on-end | When this weather type ends in a region, just before the next one begins | function(world, weather_region) |
Note on
server-on-end: by the time it runs, the region has already switched to the next weather, soweather_region:get_current_weather()returns the new weather, not the one that just ended. The hook is for cleanup tied to the weather type it’s attached to (which you already know); use theweather_regionfor spatial sampling.
delta_ns is the tick delta in nanoseconds (delta_ns * 1e-9 = seconds).
The Weather Region Object
lua_data
A persistent table for per-region script state, kept across ticks and across weather changes — the standard place for accumulators (see the pattern below). Field changes stick immediately; assigning a new table replaces it.
get_current_weather()
Returns a table describing the active weather:
| Key | Type | Description |
|---|---|---|
name | string | Weather type name. |
duration_ns | number | Total duration rolled for this weather spell, in nanoseconds. |
current_time_ns | number | How long this spell has been running, in nanoseconds. |
min_duration_ns / max_duration_ns | number | The configured duration range. |
transition_out_duration_ns | number | Configured fade-out time. |
transition_in_duration_ns | number | Configured fade-in time (0 if unset). |
weight | number | The relative weight this weather has in the biome’s weighted weather pick-one. |
All times are nanoseconds.
select_n_random_locations(n)
Returns a list (1-indexed) of n random locations anywhere within the region — any depth, not just the surface. Sampling is with replacement: the same spot can appear more than once. Returns an empty list if the region has no loaded area.
select_n_random_surface_locations(n)
Same, but samples only surface positions (the top block of a column) — the right choice for rain, snow, and lightning-style effects. Returns an empty list if no surface is loaded.
The returned locations carry x and y in block coordinates. Their world field is empty — use the world object your hook already received to act on them.
Rate-Independent Effects (Accumulator Pattern)
The server tick rate varies between machines, so never apply “one effect per tick” — the effect rate would differ wildly between platforms. Instead accumulate using the tick delta and apply a whole number of effects when the accumulator crosses 1:
return function(world, weather_region, delta_ns)
local data = weather_region.lua_data
data.acc = data.acc or 0
local samples_per_second = 6
-- Cap the delta so a lag spike doesn't dump a burst of effects at once.
local delta_sec = math.min(delta_ns * 1e-9, 0.5)
data.acc = data.acc + samples_per_second * delta_sec
local raw = math.floor(data.acc)
if raw <= 0 then return end
-- Drain the full amount, but cap how much work one tick does.
local samples = math.min(raw, 10)
data.acc = data.acc - raw -- subtract raw, NOT samples, or the rate drifts
local locations = weather_region:select_n_random_surface_locations(samples)
for _, loc in ipairs(locations) do
local below = world:get_block_at(loc.x, loc.y, 1)
if below.id ~= nil then
-- e.g. accumulate snow, freeze water, etc.
end
end
end
Two rules keep this correct on every machine:
- Subtract the full floored amount from the accumulator (
raw), not the cappedsamplesvalue — otherwise any spike permanently locks the rate to one sample per tick. - If an effect itself builds up over repeated samples (e.g. snow depth per spot), add a fixed amount per sample (
1.0 / samples_per_secondworth), not an amount scaled bydelta_sec— per-sample deltas are tick-rate dependent.
See Also
- weather-config.md — declaring weather types
- lua-server-world.md — the world object passed to weather hooks