Skip to content

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:

lua
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_scripts file, so the same call in a server script fails.
  • rm_fireworks must be started first. Put ensure rm_fireworks before 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 raises attempt 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.

lua
-- your_resource/config.lua
YourConfig = {}
YourConfig.Fireworks = {
    [1] = { item = "firework_1" },
    [5] = { coords = vector3(2508.15, -1201.93, 52.48) },
}

Returned functions

FunctionReturnsBehaviour
Start(fireworkId, canCancel)true / false / nilRuns the full plant → fuse → wiring → detonator sequence at the player's position. Consumes one item. Blocks the calling thread
Stop()nilClears state flags, clears ped tasks, removes the walk style, deletes props and rope, hides the prompt. Does not refund
IsInFireworks()booleanTrue from Start until it returns
IsInFuses()booleanTrue from the fuse prompt until the wiring animation finishes
IsInDetonator()booleanTrue 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

SituationReturn
Unknown fireworkIdfalse, plus a red console line. No item consumed
A sequence is already runningnil. Silently dropped, no item consumed
Cancelled during the fuse stage, or Stop() called therenil
Cancelled at the detonator, or Stop() called therefalse
Plunger pressed and the show firedtrue

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:

MilestoneDelay
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 stepNo 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

lua
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

EventSidePayloadStatus
rm_fireworks:UseFireworksclientfireworkIdPublic. What the usable item fires. Runs Start(id, true), consuming the item
rm_fireworks:LaunchFireworksclientidPublic. Plays a fixed-coords show. Consumes nothing
rm_fireworks:LST:ExplosionclientcoordsPublic hook, fired locally. An empty stub for you to fill in
rm_fireworks:LST:Explosionservercoords, fireworkIdPublic hook. Also the broadcast and logging trigger
rm_fireworks:SyncFireworksclientsenderId, fireworkId, coordsInternal. Sent only when the sender genuinely consumed that item
rm_fireworks:RemoveFireworksserverfireworkIdInternal. Removes one item; replies with an abort on failure
rm_fireworks:GiveFireworksserverfireworkIdInternal. Refunds only against a recorded in-flight pair
rm_fireworks:AbortFireworksclientnoneInternal. 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 eventWhat a client gains
RemoveFireworksNothing. It only destroys one of the caller's own items
GiveFireworksNothing. It pays out only against a matching in-flight record, and clears it on payout, so it cannot be replayed
LST:ExplosionA 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.

lua
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:

lua
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

lua
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

lua
RegisterCommand("firework", function(source, args)
    local index = tonumber(args[1])
    if not index then return end
    TriggerClientEvent("rm_fireworks:LaunchFireworks", -1, index)
end, true)
cfg
add_ace group.admin command.firework allow

true 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.

Documentation for RedMorrow. Scripts are licensed per server — redistribution is not permitted.