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: keyWhen CalledSignature
server-updateEvery server tick while this weather is activefunction(world, weather_region, delta_ns)
server-on-startWhen this weather type begins in a regionfunction(world, weather_region)
server-on-endWhen this weather type ends in a region, just before the next one beginsfunction(world, weather_region)

Note on server-on-end: by the time it runs, the region has already switched to the next weather, so weather_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 the weather_region for 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:

KeyTypeDescription
namestringWeather type name.
duration_nsnumberTotal duration rolled for this weather spell, in nanoseconds.
current_time_nsnumberHow long this spell has been running, in nanoseconds.
min_duration_ns / max_duration_nsnumberThe configured duration range.
transition_out_duration_nsnumberConfigured fade-out time.
transition_in_duration_nsnumberConfigured fade-in time (0 if unset).
weightnumberThe 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:

  1. Subtract the full floored amount from the accumulator (raw), not the capped samples value — otherwise any spike permanently locks the rate to one sample per tick.
  2. 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_second worth), not an amount scaled by delta_sec — per-sample deltas are tick-rate dependent.

See Also