RedM Emotes Developer API
The supported surface
Everything in this table is intended for third-party use. Everything else in the resource is internal.
| Surface | Side | Purpose |
|---|---|---|
exports.rm_emotes:Play(dict, name, duration, isLoop) | client | Play an arbitrary animation |
exports.rm_emotes:PlayById(emoteId) | client | Play a library row by database id |
exports.rm_emotes:PlayByCommand(slug) | client | Play a library row by its /e slug |
exports.rm_emotes:Stop() | client | Stop whatever is playing |
exports.rm_emotes:Open() | client | Open the menu |
exports.rm_emotes:Close() | client | Close the menu |
exports.rm_emotes:IsOpen() | client | Is the menu open |
exports.rm_emotes:Rebuild() | server | Re-read the table and push to every player |
rm_emotes:client:open | net event | Open the menu, from either side |
rm_emotes:server:rebuildLibrary | server event | Same as Rebuild(), for TriggerEvent |
rm_emotes:server:pullLibrary | net event | Make the calling player re-pull their library |
rm_emotes:server:pullCollection | net event | Make 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:
if GetResourceState('rm_emotes') == 'started' then
exports.rm_emotes:Stop()
endClient exports
| Export | Arguments | Returns | Behaviour |
|---|---|---|---|
Play | dict, name, duration or nil, isLoop or nil | nothing | Builds a one-off anim row and plays it. duration is ms; <= 0 holds indefinitely |
PlayById | emoteId number | true if a row with that id was found | Searches only the database library pushed to this player. Prop and shared emotes are unreachable |
PlayByCommand | slug string | true if a row matched | Matches the /e slug, lower-cased. Searches the database library first, then prop and shared rows |
Stop | none | nothing | Cancels adjust mode, stops a prop or shared emote, stands the player up, then clears tasks and unfreezes. Does not clear a walk style |
Open | none | nothing | Opens the menu and takes NUI focus. Silently does nothing if already open, mounted, or in adjust mode |
Close | none | nothing | Closes the menu, releases NUI focus, dismisses the mannequin. Not guarded by menu state |
IsOpen | none | boolean | Current 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:
if exports.rm_emotes:IsOpen() then
exports.rm_emotes:Close()
endFive 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.
truemeans "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.Playbypasses the library entirely, so it ignoreshiddenandrestricted. 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
ClearPedTaskson 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 ownTaskPlayAnimon top, and a walk style touches nothing but theMP_Style_*natives. - Recent tab.
Playhas no emote id, so it is not recorded.PlayByIdandPlayByCommandare. All three set the "last emote", so Repeat and/adjustanimact on what you played.
A shop resource greets the player
-- 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
-- my_job/server.lua
RegisterNetEvent('my_job:server:startShift', function()
TriggerClientEvent('my_job:client:workPose', source)
end)-- 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
TriggerEvent('rm_emotes:client:open') -- from a client script
TriggerClientEvent('rm_emotes:client:open', source) -- from a server scriptBoth 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.
-- 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
| Event | Direction | Notes |
|---|---|---|
rm_emotes:client:open | either → client | Opens the menu |
rm_emotes:server:rebuildLibrary | server → server | Server-only handler. A client cannot reach it |
rm_emotes:server:pullLibrary | client → server | Re-pushes that player's library. Serves the cache, no database hit |
rm_emotes:server:pullCollection | client → server | Re-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
| Event | Side | Effect |
|---|---|---|
vorp:SelectedCharacter | client | Re-pull library and collection, re-apply the walk style after 6 s |
RSGCore:Client:OnPlayerLoaded | client | Same |
redemrp:playerLoaded | server | After 1 s, push library and collection to that player |
RSGCore:Server:OnPlayerLoaded | server | Same |
ctrl:closeAllMenu | client | Closes the menu |
ctrl_basic:keyPress | client | data.key == 'F5' opens the menu, regardless of your key config |
Knowing the player is busy
| Signal | When |
|---|---|
LocalPlayer.state.isInMenu | Set 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:
-- 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 theopen/sync/close/adjust/gizmomessages exist for the bundled interface only. - The client globals
RMBridge,RMExtras,RMAdjust,RMSeatsandShowcase. Plain Lua globals inside the resource, not exports, not reachable from another resource. - Library and collection payload shapes, including the derived
commandandcommand_customfields. - Synthetic ids for prop and shared emotes. Generated at runtime from
90000and 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 browserlocalStoragekeyrm_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 resource | What happens | What to do |
|---|---|---|
| Another emote menu | Both poll the same control hashes and neither poll is exclusive — both react to the same press | Set 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 load | Run one of the two. Players clear this one with "Normal Walk" |
| A chair or sitting resource | Two "Sit" prompts on the same props | RM.Seats.enabled = false in shared/seats.lua |
| Crafting, inventory, anything playing its own animation | Starting a plain animation or a scenario calls ClearPedTasks, cancelling your task — and your TaskPlayAnim cancels the emote. Stop() also unfreezes a ped another script froze | Call Stop() before your own animation, and treat the player ped as shared state |
| A raw-keyboard bind resource | This 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 otherwise | RM.BindListener = 'ctrl', and RM.ShiftHotkeys = false |
A resource using RegisterKeyMapping | This one registers exactly one: rm_gizmo, default G | Change RM.Adjust.gizmoKey, or gizmo = false. A key a player already saved does not move |
A resource owning /e, /emotes or /adjustanim | Not an error, but only one handler ends up running | Rename via RM.Command.name, RM.SlashCommand.name, RM.Adjust.command. The rm_gizmo name is not configurable |
ctrl_basic | F5 in a keyPress payload opens the menu regardless of your key config | Nothing to configure — do not send F5 through ctrl_basic |
| Another resource using the same tables | This one owns rm_emotes and rm_emote_favs, with a cascade between them | Do not share those table names |
| oxmysql | If it starts after this resource, the first library load fails | ensure oxmysql first |