Skip to content

RedM Emotes Developer API

The supported surface

Everything in this table is intended for third-party use. Everything else in the resource is internal.

SurfaceSidePurpose
exports.rm_emotes:Play(dict, name, duration, isLoop)clientPlay an arbitrary animation
exports.rm_emotes:PlayById(emoteId)clientPlay a library row by database id
exports.rm_emotes:PlayByCommand(slug)clientPlay a library row by its /e slug
exports.rm_emotes:Stop()clientStop whatever is playing
exports.rm_emotes:Open()clientOpen the menu
exports.rm_emotes:Close()clientClose the menu
exports.rm_emotes:IsOpen()clientIs the menu open
exports.rm_emotes:Rebuild()serverRe-read the table and push to every player
rm_emotes:client:opennet eventOpen the menu, from either side
rm_emotes:server:rebuildLibraryserver eventSame as Rebuild(), for TriggerEvent
rm_emotes:server:pullLibrarynet eventMake the calling player re-pull their library
rm_emotes:server:pullCollectionnet eventMake the calling player re-pull their saved emotes

Exports are keyed on the resource folder name. All of the above assume rm_emotes; rename the folder and you must rename your calls too.

Guard your calls if the resource is optional on your server:

lua
if GetResourceState('rm_emotes') == 'started' then
    exports.rm_emotes:Stop()
end

Client exports

ExportArgumentsReturnsBehaviour
Playdict, name, duration or nil, isLoop or nilnothingBuilds a one-off anim row and plays it. duration is ms; <= 0 holds indefinitely
PlayByIdemoteId numbertrue if a row with that id was foundSearches only the database library pushed to this player. Prop and shared emotes are unreachable
PlayByCommandslug stringtrue if a row matchedMatches the /e slug, lower-cased. Searches the database library first, then prop and shared rows
StopnonenothingCancels adjust mode, stops a prop or shared emote, stands the player up, then clears tasks and unfreezes. Does not clear a walk style
OpennonenothingOpens the menu and takes NUI focus. Silently does nothing if already open, mounted, or in adjust mode
ClosenonenothingCloses the menu, releases NUI focus, dismisses the mannequin. Not guarded by menu state
IsOpennonebooleanCurrent menu state

Close() is not guarded by menu state

Calling it while the menu is already closed still runs SetNuiFocus(false, false), which releases NUI focus globally and can break another resource's open interface. Guard it, especially from a generic close-all-menus handler:

lua
if exports.rm_emotes:IsOpen() then
    exports.rm_emotes:Close()
end

Five things to know before you call them:

All three play exports yield

Play, PlayById and PlayByCommand run the same code path: for a plain animation row it requests the animation dictionary and waits up to 5000 ms, then gives up silently. PlayById and PlayByCommand deliver their boolean only after that wait, and still return true when the dictionary never loaded. Call them from a command handler, event handler or thread — never from a per-frame loop.

  • These are client exports. A server script cannot call them. Route through your own net event.
  • true means "the row exists", not "the animation started". Both lookups return before playback is gated — a plain animation, scenario, prop or shared emote is refused while mounted, and a shared emote is refused with no partner in range.
  • Play bypasses the library entirely, so it ignores hidden and restricted. If your resource exposes a dict and clip to players, you own that permission check.
  • Playback replaces playback. Playing a plain animation or a scenario calls ClearPedTasks on the player first, and a plain animation also stops any active prop or shared emote. Walk styles and prop or shared emotes do not clear ped tasks — a prop emote layers its own TaskPlayAnim on top, and a walk style touches nothing but the MP_Style_* natives.
  • Recent tab. Play has no emote id, so it is not recorded. PlayById and PlayByCommand are. All three set the "last emote", so Repeat and /adjustanim act on what you played.

A shop resource greets the player

lua
-- my_shop/client.lua
RegisterNetEvent('my_shop:client:openCounter', function()
    exports.rm_emotes:Play('ai_gestures@gen_female@standing@speaker',
        'neutral_get_attention_l_001', 2500, false)
end)

A job script forces a pose, driven from the server

lua
-- my_job/server.lua
RegisterNetEvent('my_job:server:startShift', function()
    TriggerClientEvent('my_job:client:workPose', source)
end)
lua
-- my_job/client.lua
RegisterNetEvent('my_job:client:workPose', function()
    -- by slug, so an admin can swap the pose in the editor
    -- without anyone touching this resource
    if not exports.rm_emotes:PlayByCommand('leanback') then
        exports.rm_emotes:Play('script_mp@last_round@photos', 'pose2_m03', -1, true)
    end
end)

RegisterNetEvent('my_job:client:endShift', function()
    exports.rm_emotes:Stop()
end)

PlayByCommand returning false is the honest signal that the player cannot see that row — either it does not exist, or it is hidden or restricted and was never sent to this client.

Opening the menu from your own resource

lua
TriggerEvent('rm_emotes:client:open')                 -- from a client script
TriggerClientEvent('rm_emotes:client:open', source)   -- from a server script

Both honour exactly the same guards as the key. The resource also closes on ctrl:closeAllMenu, which exists for the ctrl_basic ecosystem.

The server export

Rebuild() runs SELECT * FROM rm_emotes, re-derives every /e slug, then re-sends the library to every connected player, filtered per player.

lua
-- my_admin/server.lua
RegisterCommand('hidedances', function(source)
    if not IsPlayerAceAllowed(source, 'rm_emotes.admin') then return end
    MySQL.query.await("UPDATE `rm_emotes` SET hidden = 1 WHERE category = 'dance'")
    exports.rm_emotes:Rebuild()
end, true)

Two limits:

  • It pushes the library only. It does not re-push anyone's saved emotes.
  • It is not a net event, on purpose, so clients cannot force full-table reloads.

TriggerEvent('rm_emotes:server:rebuildLibrary') calls the same function, useful when you do not want a hard dependency on the export name.

Events

Events you may trigger

EventDirectionNotes
rm_emotes:client:openeither → clientOpens the menu
rm_emotes:server:rebuildLibraryserver → serverServer-only handler. A client cannot reach it
rm_emotes:server:pullLibraryclient → serverRe-pushes that player's library. Serves the cache, no database hit
rm_emotes:server:pullCollectionclient → serverRe-pushes their saved emotes

pullCollection is one database query per call, unthrottled

Use it on character load, not on a timer. It is the cheapest amplification vector in the resource.

Both pull* events act on source only — a client cannot use them to read another player's data.

Events it sends, which you should treat as read-only internals

rm_emotes:client:library, rm_emotes:client:collection, and the sharedRequest / sharedAccepted / sharedDeclined / sharedStop relays.

You can add a handler, but the row shape is an implementation detail and may change between versions. Do not build a UI on top of it.

Events it listens for, owned by someone else

EventSideEffect
vorp:SelectedCharacterclientRe-pull library and collection, re-apply the walk style after 6 s
RSGCore:Client:OnPlayerLoadedclientSame
redemrp:playerLoadedserverAfter 1 s, push library and collection to that player
RSGCore:Server:OnPlayerLoadedserverSame
ctrl:closeAllMenuclientCloses the menu
ctrl_basic:keyPressclientdata.key == 'F5' opens the menu, regardless of your key config

Knowing the player is busy

SignalWhen
LocalPlayer.state.isInMenuSet true / false on open / close. Readable from any client script on that player's machine. Not cleared if the resource is stopped or restarted while the menu is open — the flag stays true until the player reconnects. Treat it as a hint, not a lock, and do not gate anything irreversible on it
SetNuiFocus(true, true) / (false, false)On open / close
TriggerEvent('ctrl_basic:focus', ...)On open / close

Adjust mode also takes NUI focus while the gizmo is on, freezes the ped for the whole session, and can leave it frozen after a confirmed placement until the emote is stopped. exports.rm_emotes:Stop() always unfreezes.

Re-syncing on your own character load

No load hook fires for an unknown framework, so the client pulls once when the resource starts. Add your own:

lua
-- my_framework/client.lua
RegisterNetEvent('my_framework:client:characterSelected', function()
    Wait(1000) -- let the job data settle server-side first
    TriggerServerEvent('rm_emotes:server:pullLibrary')
    TriggerServerEvent('rm_emotes:server:pullCollection')
end)

There is no per-player push entry point on the server side. Rebuild() pushes the library to everyone and does not touch collections.

What is not public API

These work today, are not a contract, and can change in any release without notice:

  • NUI callbacks and messages. The https://rm_emotes/<name> callbacks and the open / sync / close / adjust / gizmo messages exist for the bundled interface only.
  • The client globals RMBridge, RMExtras, RMAdjust, RMSeats and Showcase. Plain Lua globals inside the resource, not exports, not reachable from another resource.
  • Library and collection payload shapes, including the derived command and command_custom fields.
  • Synthetic ids for prop and shared emotes. Generated at runtime from 90000 and not stable across config edits. Never store one.
  • The shared emote relay events. Server-side validation is limited to "the target is a different, connected player and the payload is a table"; the definition itself is client-supplied.
  • KVP keys rm_emotes:recent, rm_emotes:walk, rm_emotes:preview, and the browser localStorage key rm_emotes:layout.
  • The server-side library cache and its helpers.

In the escrow edition client/, server/ and ui/ are encrypted, so patching internals is not an option there in any case.

Does it conflict with…

Other resourceWhat happensWhat to do
Another emote menuBoth poll the same control hashes and neither poll is exclusive — both react to the same pressSet RM.Keys.open.active = false and cancel.active = false, or change the hashes
A walk-style resource (vorp_walkanim)Both drive the same MP_Style_* natives, so the last writer wins. This one also re-applies the stored style 4 s after start and 6 s after character loadRun one of the two. Players clear this one with "Normal Walk"
A chair or sitting resourceTwo "Sit" prompts on the same propsRM.Seats.enabled = false in shared/seats.lua
Crafting, inventory, anything playing its own animationStarting a plain animation or a scenario calls ClearPedTasks, cancelling your task — and your TaskPlayAnim cancels the emote. Stop() also unfreezes a ped another script frozeCall Stop() before your own animation, and treat the player ped as shared state
A raw-keyboard bind resourceThis one polls IsRawKeyDown for letter/digit binds and SHIFT+1..5. It stands down while the menu is open, while any NUI has focus and during adjust mode — but not otherwiseRM.BindListener = 'ctrl', and RM.ShiftHotkeys = false
A resource using RegisterKeyMappingThis one registers exactly one: rm_gizmo, default GChange RM.Adjust.gizmoKey, or gizmo = false. A key a player already saved does not move
A resource owning /e, /emotes or /adjustanimNot an error, but only one handler ends up runningRename via RM.Command.name, RM.SlashCommand.name, RM.Adjust.command. The rm_gizmo name is not configurable
ctrl_basicF5 in a keyPress payload opens the menu regardless of your key configNothing to configure — do not send F5 through ctrl_basic
Another resource using the same tablesThis one owns rm_emotes and rm_emote_favs, with a cascade between themDo not share those table names
oxmysqlIf it starts after this resource, the first library load failsensure oxmysql first

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