RedM Fireworks Developer API
Everything below is client-side unless a section says otherwise. All samples go into your own resource — rm_fireworks ships no commands and no ACE checks of its own.
The export
The resource exposes exactly one export, a getter returning a table of functions:
local RM_FIREWORKS = exports.rm_fireworks:RM_FIREWORKS()That is the only supported retrieval line. There is no exports.rm_fireworks:Start(...) and no per-function export — calling one raises No such export.
- Client-side only. It is registered in a
client_scriptsfile, so the same call in a server script fails. rm_fireworksmust be started first. Putensure rm_fireworksbefore your resource, or fetch the table lazily inside your handler.- Caching the table in a local is fine, but re-fetch it after
restart rm_fireworks— the old references point at a dead runtime. - There is no server-side export. There is no supported way to ask which framework it bound to.
RM_Config does not cross resource boundaries
The single thing integrators get wrong most often.
config/config.lua is declared in shared_scripts in rm_fireworks' manifest, which loads it into that resource's own client and server states. shared_scripts means "shared between the client and server sides of this resource", not "shared between resources".
Your resource therefore cannot read RM_Config:
- Load order does not help.
- Globals are per-state.
RM_Config.Fireworks[index]in your code raisesattempt to index a nil value (global 'RM_Config'). - The same applies to
RM_UTILS,RM_CORE,RM_InFireworks— none are visible, none are exported.
Two options, both meaning you write the values down a second time:
Hardcode the ids. If all you need is "call Start with id 3", pass 3.
Mirror what you need. A hand-maintained copy in your own config, kept in sync by hand — nothing checks it for you.
-- your_resource/config.lua
YourConfig = {}
YourConfig.Fireworks = {
[1] = { item = "firework_1" },
[5] = { coords = vector3(2508.15, -1201.93, 52.48) },
}Returned functions
| Function | Returns | Behaviour |
|---|---|---|
Start(fireworkId, canCancel) | true / false / nil | Runs the full plant → fuse → wiring → detonator sequence at the player's position. Consumes one item. Blocks the calling thread |
Stop() | nil | Clears state flags, clears ped tasks, removes the walk style, deletes props and rope, hides the prompt. Does not refund |
IsInFireworks() | boolean | True from Start until it returns |
IsInFuses() | boolean | True from the fuse prompt until the wiring animation finishes |
IsInDetonator() | boolean | True from the detonator prompt until Start returns |
The state getters lag the prompts
RM_InFuses is not cleared when the player presses use. The handler hides the prompt, detaches the spool, runs the whole ~8.8 s wiring chain, and only then sets it false — so IsInFuses() stays true for that whole window with no prompt on screen. IsInDetonator() behaves the same way at the other end.
If you are gating on "is this player busy", use IsInFireworks() — it is the only one that covers the sequence without the lag.
Start return values
| Situation | Return |
|---|---|
Unknown fireworkId | false, plus a red console line. No item consumed |
| A sequence is already running | nil. Silently dropped, no item consumed |
Cancelled during the fuse stage, or Stop() called there | nil |
Cancelled at the detonator, or Stop() called there | false |
| Plunger pressed and the show fired | true |
Note the ambiguity: false means either unknown id or cancelled at the detonator; nil means either already busy or cancelled during the fuse. The only guard you can perform from outside is IsInFireworks().
Start ignores an entry's coords
Start always plants at the caller's feet. Start(5, true) on the shipped config does not launch entry 5's fixed-coords show — it runs the full sequence where the player is standing, for an entry that has no item. Only LaunchFireworks reads coords.
canCancel
It controls one thing: whether the cancel prompt is shown and honoured on the detonator. Cancelling during the fuse stage is always available.
In the source this parameter is declared as removeItem. That name is misleading — it has nothing to do with item removal.
Timing
Start blocks:
| Milestone | Delay |
|---|---|
Start → fuse prompt | ~15.9 s of scripted animation |
| Fuse prompt → detonator prompt | ~10.6 s after the player presses use at range |
| Either prompt → next step | No timeout. The loops poll forever |
| Plunger → first burst | ~2.6 s |
Plunger → Start returns | ~5.1 s. The show outlives the return |
Call Start from an event handler, a command callback or a thread — never from a tight loop or code that must return promptly.
Item consumption
Once Start passes its id check and busy guard, it fires the server's removal event before any animation. There is no way to run the sequence without consuming the item, and the item leaves the inventory at the moment of use.
If the entry has item = false, the server ignores the event entirely: nothing is consumed and nothing is recorded, so the detonation is not broadcast and a cancel refunds nothing. The sequence still runs in full.
Usage example
local RM_FIREWORKS = nil
local function GetFireworksApi()
if not RM_FIREWORKS then
RM_FIREWORKS = exports.rm_fireworks:RM_FIREWORKS()
end
return RM_FIREWORKS
end
RegisterNetEvent("your_resource:StartFirework", function(fireworkId)
local api = GetFireworksApi()
if api.IsInFireworks() then return end
-- rm_fireworks checks nothing about the player's state. Do it here.
local ped = PlayerPedId()
if IsPedDeadOrDying(ped, true) or IsPedOnMount(ped) or IsPedInAnyVehicle(ped, false) then
return
end
local result = api.Start(fireworkId, true)
if result == true then
-- detonated
elseif result == false then
-- unknown id, or cancelled on the detonator
else
-- cancelled during the fuse stage, or the client was busy
end
end)Your manifest must declare game 'rdr3', and any RedM resource that does must carry the rdr3_warning line verbatim or the server refuses to start it — see The RedM fxmanifest.
Events
| Event | Side | Payload | Status |
|---|---|---|---|
rm_fireworks:UseFireworks | client | fireworkId | Public. What the usable item fires. Runs Start(id, true), consuming the item |
rm_fireworks:LaunchFireworks | client | id | Public. Plays a fixed-coords show. Consumes nothing |
rm_fireworks:LST:Explosion | client | coords | Public hook, fired locally. An empty stub for you to fill in |
rm_fireworks:LST:Explosion | server | coords, fireworkId | Public hook. Also the broadcast and logging trigger |
rm_fireworks:SyncFireworks | client | senderId, fireworkId, coords | Internal. Sent only when the sender genuinely consumed that item |
rm_fireworks:RemoveFireworks | server | fireworkId | Internal. Removes one item; replies with an abort on failure |
rm_fireworks:GiveFireworks | server | fireworkId | Internal. Refunds only against a recorded in-flight pair |
rm_fireworks:AbortFireworks | client | none | Internal. Calls Stop() |
RemoveFireworks and GiveFireworks do not coerce their argument — triggering them with a string such as "1" misses the lookup and returns silently. UseFireworks, LaunchFireworks and SyncFireworks do run theirs through tonumber.
None of these events are authenticated
Every event is a plain RegisterNetEvent with no identity, permission or rate checks. Be precise about what that is worth to an attacker:
| Forged event | What a client gains |
|---|---|
RemoveFireworks | Nothing. It only destroys one of the caller's own items |
GiveFireworks | Nothing. It pays out only against a matching in-flight record, and clears it on payout, so it cannot be replayed |
LST:Explosion | A junk line in your JD_logsV3 channel, and the clearing of the caller's own pending refund. The broadcast is gated on the in-flight record |
Treat LST:Explosion as an untrusted client claim
Never pay money, items, XP or reputation from it without your own server-side validation.
Hooking the detonation
Client side
Fired with TriggerEvent — local, not networked — with the world coords as the only argument. Use a plain AddEventHandler.
AddEventHandler("rm_fireworks:LST:Explosion", function(coords)
-- coords is the planted crate's position, read when the plunger was pressed.
-- This is the natural place to add a bang: the resource plays no audio at all.
end)This runs only on the client that pressed the plunger — players watching a synced show receive SyncFireworks instead, not this. The client hook receives only coords, not the id.
RDR3 has its own native names
GTA V camera shakes, sound sets and audio banks do not carry over, and an unrecognised name usually fails silently rather than erroring. Verify every native you add against your own server build.
Server side
The same name is a net event carrying a second argument:
RegisterNetEvent("rm_fireworks:LST:Explosion", function(coords, fireworkId)
local src = source
-- WARNING: any client can trigger this with any payload.
end)What the resource itself validates here:
- The broadcast is validated and gated on
SyncFireworks. A forged event cannot force a show onto anyone. - The log line is not validated. It is written on every occurrence, including forged ones.
- The in-flight record is cleared on every occurrence — a forger only forfeits their own refund.
Building a /firework command
The resource registers no commands. Both variants drive LaunchFireworks, so the entry must have coords set.
Client only — for tuning
RegisterCommand("firework", function(_, args)
local index = tonumber(args[1])
if not index then return end
-- Wrapped in a thread on purpose: LaunchFireworks runs the show inline,
-- so without this your callback blocks for the whole show.
Citizen.CreateThread(function()
TriggerEvent("rm_fireworks:LaunchFireworks", index)
end)
end, false)Server broadcast — for an actual event
RegisterCommand("firework", function(source, args)
local index = tonumber(args[1])
if not index then return end
TriggerClientEvent("rm_fireworks:LaunchFireworks", -1, index)
end, true)add_ace group.admin command.firework allowtrue means restricted, not "admin only"
The third argument marks the command denied to everyone by default — including you — until an ace grants it. Passing false or omitting it leaves it open to every player, which for the broadcast variant means anyone can fire a server-wide show. Getting this backwards is the usual way an event command ends up open to the whole server.
Register only one variant under the name. If you register both, the client one wins for players and only the server console falls through.
Cost note. Each receiving client spawns number local rocket objects, each on its own thread with a 10 ms velocity re-apply and no distance culling. Every burst spawns two particle effects, not one. Tune number down before broadcasting to a full server.
Gotchas and limitations
There are no state guards. Anywhere. Start checks two things: the id exists, and a sequence is not already running. That is the entire list. Mounted, in a wagon, swimming, in combat, falling, hogtied, dead — none of it is checked, before or during. Do your checks before you call Start.
LaunchFireworks is silently ignored on a client mid-sequence, and for entries whose coords is falsy. The four shipped item entries have coords = false, so /firework 1 through 4 do nothing and report nothing.
LaunchFireworks runs the show inline; SyncFireworks does not. Triggering it locally blocks your calling code for the whole show. Wrap it in a thread.
The reverse guard does not exist. Start does not check whether a coords or synced show is already playing, and the show loop sets none of the state flags. The two overlap freely, and there is no API to detect or stop a running show — Stop() does not touch rockets already in the air.
The state helpers do not cover fixed-coords shows. During one, all three getters return false.
Start consumes the item; Stop does not give it back. If you call Start then Stop yourself, the player is down one firework. To refund, trigger the give event explicitly from the client that consumed it, passing a number, not a string.
Stop() only takes effect from the fuse prompt onward. The plant and wiring phases are straight-line blocking chains with no state re-check — calling Stop() during either deletes the current props and the chain immediately recreates them. Do not call Start again to "restart" during the plant phase: Stop() cleared the busy flag, so you will end up with two overlapping sequences sharing prop handles.
A failed item removal does not truly abort the client. The server sends an abort and the client calls Stop(), but the start chain has no re-check afterwards, so it rebuilds its props and can complete locally. No item is duplicated, no refund is paid and nothing is broadcast — the worst case is a cosmetic, single-client show. See Troubleshooting.
A player who disconnects mid-sequence loses the item. playerDropped clears the record without refunding. A clean restart rm_fireworks does refund.
No permissions, no cooldown, no rate limit. Patterns for adding your own are in Troubleshooting.