Skip to content

Automation and store events

Timed, auto-restoring environment events - presets, the zero-Lua Tebex path, exports, and the compliance rules.

Updated 2026-07-02

On this page

Fire a timed, auto-restoring environment change from your own scripts or a schedule - a community-goal thunderstorm, a vote-triggered fog, a nightly scheduled storm, an admin event button. It applies the change, then puts the sky back when the timer ends. One of those triggers can be a store purchase if you want it - but the feature is the timed event, not the sale.

  • Trigger it from anywhere: call ApplyEnvironmentPreset or StartTimedOverride from your event script, or run the built-in env:store command from a scheduler, an RCON call, or a store package.
  • Named presets only - staff-defined in Config.Presets, never raw weather from an outside caller.
  • Idempotent, per-axis, ephemeral: a repeated trigger (same reference) is a safe no-op; weather, time, and blackout stack independently; nothing survives a restart - a triggered storm never becomes permanent.
  • Never fights staff: an override refuses an axis an admin has locked, and if staff explicitly change an axis while a timed event holds it, the event on that axis ends (overrideEnded fires with reason staff_override) instead of silently snapping the staff change back. Staff always win.
Tip

Test it in five seconds, no store needed. Run this in the server console:

env:store storm_event 5 test

If the sky changes, the feature works end to end - anything left is your own trigger's wiring. env:doctor prints a store/automation line (on/off, presets loaded, active count) for the same reason.

The zero-Lua Tebex path

Point a Tebex package command at the built-in preset-only command:

env:store <presetName> <durationMinutes> <reference>

This is Tebex's command path - a command Tebex runs on your server console on purchase (RCON / game-server command delivery), NOT the separate FiveM "Server Wrapper" redeem product. In your package's command list, add one line:

env:store storm_event 30 {transaction}

It applies registered presets only, clamps the duration to Config.StoreIntegration min/max, and uses {transaction} as the idempotency key. Map your packages to preset names in Config.Presets. Verify against your own Tebex setup before selling - command variables and delivery differ by integration type. If you use the Server Wrapper redeem flow, drive it from your fulfillment resource via the exports below instead.

Warning

Watch for {transaction} not substituting. If your Tebex integration delivers the literal string {transaction} instead of a real id, every purchase arrives with the SAME reference - so the second purchase is deduped as already_applied and that customer's event never fires. If env:override list shows the same ref on every event, your {transaction} variable is not being substituted; fix it on the Tebex side.

Admins can see and cancel active overrides from the console (for example, a wrong preset went out):

env:override list
env:override clear <id|source|all>

Exports (server, trusted)

-- Define a reusable preset. spec: { weather?, intensity?, hour?, blackout?, label? }
local ok = exports['dzr-environment']:RegisterEnvironmentPreset(name, spec)

-- Timed, tagged, auto-restoring override - the core primitive.
local ok, reason, overrideId = exports['dzr-environment']:StartTimedOverride(spec, {
    durationMinutes = 30,      -- REQUIRED (omitting it returns false, 'invalid_spec')
    source          = 'tebex', -- tag for logs/clearing (defaults to 'unknown')
    reference       = txnId,   -- idempotency key (a repeat is a safe no-op)
    announce        = nil,     -- fire a 'notice' event; nil = follow Config.StoreIntegration.announce
    respectLock     = true,    -- refuse if an admin lock owns that axis (default)
    priority        = 0,       -- stack order when overrides overlap
})
-- reason: 'ok' | 'already_applied' | 'axis_locked' | 'axis_disabled' | 'invalid_spec' | ...

-- Apply a preset now: with durationMinutes it is timed; without, permanent
-- (goal/vote triggers).
local ok, reason, id = exports['dzr-environment']:ApplyEnvironmentPreset(name, options)

exports['dzr-environment']:ClearTimedOverride(overrideIdOrSource)  -- end early (id or source tag)
local list = exports['dzr-environment']:GetActiveOverrides()
-- { { id, source, spec, endsAt, remainingSeconds, ... }, ... }
local done = exports['dzr-environment']:IsReferenceProcessed(reference)
Note

On a duplicate reference, the third return of StartTimedOverride is the existing override's id only while that override is still active - treat it as optional and never assume it is non-nil.

Events

dzr-environment:server:overrideStarted  { id, source, reference, spec, startedAt, endsAt, durationMinutes }
dzr-environment:server:overrideEnded    { id, source, reference, reason }  -- expired|cleared|staff_override|restart
dzr-environment:server:presetApplied    { name, source, reference, spec }
dzr-environment:server:notice           { kind, source, spec, endsAt, label }  -- only when announce is on
dzr-environment:server:audit            { action, source, reference, actor, result, detail }  -- SERVER-ONLY

The :client: variants broadcast to all players with one privacy difference: reference (your store's transaction id) is stripped from client payloads, and audit never leaves the server at all - other players have no business reading your customers' transaction ids. Server-side listeners always get the full payload.

The notice event is the player-facing "a storm just rolled in" hook (gated by announce); audit is the machine hook a logger or refund listener subscribes to. All events are gated by Config.BroadcastEvents - turning that off also silences your Discord announcer and audit logging.

Rules

  • Idempotent by reference. Pass the transaction id; a repeat returns ok = true, 'already_applied' - treat it as success. The cache holds the reference string only (no personal data), bounded and TTL'd. It is not a purchase database.
  • Per-axis. Weather, time, and blackout stack independently; ending one affects that axis only. Weather and blackout restore to their pre-override values. Time is a hold-then-release model: a time preset holds the clock at the requested hour for the duration, then normal progression resumes from there - it does not rewind, because time never stops moving in the world.
  • Ephemeral. Overrides do not survive a restart (an overrideEnded { reason = 'restart' } fires and the persisted base loads) - a bought storm never becomes permanent.

Selling weather events: keep it compliant

If you wire this to a real store, three rules keep you on the right side of the Cfx Platform License Agreement and out of pay-to-win territory:

  • Sell fixed, named events only ("30 minutes of storm"). The buyer must know exactly what they are getting. Never randomize the outcome of a purchase - a "mystery weather" package is a loot box, which the PLA prohibits outright even when the contents are cosmetic.
  • Weather is cosmetic; think before selling darkness. A storm or fog changes the mood for everyone equally. A blackout during live gameplay is different - killing the city lights can hand a real advantage to whoever bought it (evading pursuit, cover for a robbery). We recommend keeping the blackout preset for staff events and automation, not for player purchase.
  • You own the storefront. Pricing, refunds, taxes, and PLA compliance of what you sell are yours - this resource only moves the sky and puts it back.

Need help with this? Open a ticket on Discord or read the support guide.