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
ApplyEnvironmentPresetorStartTimedOverridefrom your event script, or run the built-inenv:storecommand 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 (
overrideEndedfires with reasonstaff_override) instead of silently snapping the staff change back. Staff always win.
TipTest it in five seconds, no store needed. Run this in the server console:
env:store storm_event 5 testIf the sky changes, the feature works end to end - anything left is your own trigger's wiring.
env:doctorprints astore/automationline (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.
WarningWatch 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 asalready_appliedand that customer's event never fires. Ifenv:override listshows 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)
NoteOn a duplicate reference, the third return of
StartTimedOverrideis 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
blackoutpreset 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.