Integrating the Pose Pack
This pack streams 104 animation dictionaries and ships a table that names all of them. It does not ship a way for a player to pick one.
That is the whole shape of the thing. There is no client.lua, no config.lua and no html/ folder, so there is no menu, no chat command, no key binding and no exports. The server script is a single print. Out of the box, a player who joins your server cannot play a pose — something you write, or something you already run, has to ask the game for the clip.
This page covers the three ways to do that.
The pack's own README describes a front end that is not in this build
README.md and the comment header at the top of clips.lua both document an in-game menu, a /redmorrow command, exports['redmorrow']:Play() and :GetClips(), a Config.ForceBody option, and files named config.lua, client.lua and html/. None of those exist here — the folder contains four files and a stream/ directory, and you can confirm that in a file browser in about five seconds.
The documentation got ahead of the build. Nothing is missing from your download; the 104 .ycd files and all 99 clip rows are present and correct. Treat this page as the authority over the README, and do not spend an evening looking for config.lua.
What you have to work with
| File | What it is |
|---|---|
fxmanifest.lua | fx_version 'cerulean', game 'rdr3', lua54 'yes', dependency '/assetpacks' |
clips.lua | A data table and nothing else. Sets Config.Clips — 105 rows, 99 live |
server.lua | One line, printing [redmorrow] Female animation pack started. |
stream/ | 104 .ycd dictionaries, 452 KB total |
README.md | Stale, as above |
clips.lua and README.md are listed in escrow_ignore, so clips.lua ships readable and editable — it is a plain Lua table you can open, copy and change. The .ycd files are assets rather than script, so escrow never applied to them.
Two things about every row, which shape everything below:
- Every live entry is
mode = 'loop'withrootMotion = false. All 99. There are noonceorholdrows, and no pose moves the character off its mark. - Every live entry carries
bodies = { female = '<dict>' }and nothing else. No male variant ships anywhere in the pack.
Config.Clips is not readable from another resource
This catches people, so it is worth being blunt about.
clips.lua writes to a global called Config. In CFX every resource runs in its own Lua state, and globals do not cross that boundary. Your emote resource, your job script and your photo-mode menu cannot see Config.Clips, and there are no exports to hand it over either.
So you have exactly two ways to reach the table:
| Option | What it means |
|---|---|
| Copy it | Paste the rows into your own resource, or copy clips.lua into it and load it from your own manifest |
| Add to this one | Put your own client script inside this resource, where Config.Clips is already in scope |
Copying is a snapshot and will not follow an update of the pack. Adding a script keeps the table live but is lost when you reinstall the pack. Pick whichever you would rather redo.
Either way, the dictionaries themselves are global to the client once the resource has started — any resource can RequestAnimDict('redmorrow@pose5@f'). It is only the Lua list that is walled off.
Which route
| Route | Effort | Survives a pack update | Good when |
|---|---|---|---|
| An emote resource | Low | Yes | You already run one, and players already know how to open it |
| Your own menu or command | Medium | Yes | You want poses behind a job, a prop, or a photo mode |
| A client script inside the pack | Low | No | You want the folder to be self-contained with the least code |
Route 1 — An emote resource you already run
The cheapest route, because the hard part is already built. Every emote system reduces a custom animation to the same two values: the dictionary and the clip name inside it. You add one row per pose carrying those two strings, plus whatever that resource calls a display name, a category and a loop setting.
The generic shape of a row, with field names standing in for your resource's own:
{
label = 'Leaning on the rail',
command = 'fpose5',
dict = 'redmorrow@pose5@f',
clip = 'pose5',
duration = -1,
}Take the two strings from the animation list or straight out of clips.lua. Set the resource's loop flag — all 99 of these are loops — and leave the duration at the resource's "run until stopped" value rather than the figure in the duration column.
With RedM Emotes, the library lives in a database table and each pose is a row, so this is the SQL-and-rebuild path rather than a file edit. The three supported ways to add an entry, the exact column names, and the cache rebuild you need afterwards are documented at Three ways to add an emote.
Start this pack before or alongside your emote resource
The dictionaries have to exist on the client before a player opens the menu. Order in server.cfg is the simplest way to guarantee it.
Do not build the dictionary name from the id
'redmorrow@' .. id .. '@f' is correct for 92 of the 99 poses and wrong for seven — and all seven fail silently, with the character standing still and no error anywhere. The offenders are listed in Seven ids that do not match their dictionary. Read the pair out of the table instead of constructing it.
Route 2 — Your own menu or command
If the poses belong to something specific — a photographer job, a saloon prop, a /pose command for your own staff — drive them yourself. Playing a pose covers the native call, the dictionary wait and the stop in detail; this section is about getting from one pose to a list of 99.
Copy the rows into your own resource first, as described above, then index them by id so a command can look one up in constant time:
local poses = {
{ id = 'pose5', label = 'pose5', dict = 'redmorrow@pose5@f', clip = 'pose5' },
{ id = 'lean', label = 'lean', dict = 'redmorrow@lean@f', clip = 'lean' },
-- ... see the generator below rather than typing 99 of these
}
local byId = {}
for _, c in ipairs(poses) do
byId[c.id] = c
endA command over that index:
RegisterCommand('pose', function(_, args)
local arg = args[1]
if not arg or arg == 'stop' then
ClearPedTasks(PlayerPedId(), true, false)
return
end
local entry = byId[arg] or poses[tonumber(arg) or 0]
if not entry then
print(('no such pose: %s'):format(arg))
return
end
PlayPose(entry.dict, entry.clip) -- see Playing a pose
end, false)Accepting either an id or a position is worth the two extra characters here: several of these ids are long and none of them are memorable.
To build a visual list, iterate the same table — the row already carries everything a menu row needs:
for i, c in ipairs(poses) do
-- i, c.id, c.label, c.dict, c.clip, c.category, c.duration
AddMenuRow(i, c.label, c.id)
endRedM has no RegisterKeyMapping
If you want a key rather than a command, you need RegisterRawKeymap with a raw Windows virtual-key code. That means the binding does not appear in the game's own key settings and players cannot rebind it themselves — it is a server-side decision. More on the platform's quirks in RedM platform notes.
What the labels and categories are actually like
Before you put this list in front of a player, look at what you are about to show them:
| Field | Reality |
|---|---|
label | The raw id for almost every row — pose67, pose68, redmorrow_p1. Only the seven regard entries are capitalised, as Regard1 and so on |
category | Also the id, per row. Grouping by category gives you 99 groups of one |
id | Not sequential. pose2, pose3, pose27 and pose46 are not ids at all, and pos60 is spelled without the e |
Two rows share the label pose47 — the entry with id pose47 (which plays from redmorrow@pose46@f) and the entry with id pose47_pose47 (which plays from redmorrow@pose47@f). In a generated menu those appear as two identical rows. Rename at least one of them.
clips.lua ships editable, and nothing in this build reads label or category, so rewriting them costs you nothing and is the single highest-value hour you can spend on this pack. "Leaning on the rail" sells a pose; pose82 does not.
Do not touch dict or clip while you are in there
An RDR2 dictionary stores its own name inside the .ycd file. The strings in those two columns have to match what is baked into the asset, so they are not yours to tidy up — and pos60 is one of them.
Route 3 — A client script inside the pack
The smallest change that makes the folder self-contained. fxmanifest.lua is plain text in an escrowed resource, Config.Clips is already in scope, and there is no existing client script to collide with.
Write your script next to
clips.lua— call itposes_client.lua:lualocal byId = {} CreateThread(function() for _, c in ipairs(Config.Clips) do byId[c.id] = c end print(('[poses] %d poses indexed'):format(#Config.Clips)) end) local function playPose(entry) local dict = entry.dict RequestAnimDict(dict) local timeout = GetGameTimer() + 5000 while not HasAnimDictLoaded(dict) do if GetGameTimer() > timeout then print(('[poses] dictionary never loaded: %s'):format(dict)) return false end Wait(0) end TaskPlayAnim(PlayerPedId(), dict, entry.clip, 4.0, -4.0, -1, 1, 0.0, false, 0, false, 0, false) return true end RegisterCommand('pose', function(_, args) local arg = args[1] if not arg or arg == 'stop' then ClearPedTasks(PlayerPedId(), true, false) return end local entry = byId[arg] or Config.Clips[tonumber(arg) or 0] if entry then playPose(entry) else print(('[poses] no such pose: %s'):format(arg)) end end, false)Add it to the manifest, after
clips.lua:luaclient_scripts { 'clips.lua', 'poses_client.lua', }The order matters.
client_scriptsload in the order listed, andConfig.Clipsdoes not exist untilclips.luahas run.Restart the resource.
/pose pose5,/pose 1and/pose stopnow work, with no second resource involved.
RDR3's TaskPlayAnim takes thirteen arguments, which is the main thing to get right if you adapt the call above. The seventh is the flag word, and RDR3's flag values are not GTA V's — a number copied out of a FiveM resource will do something other than what you expect. Duration -1 is what makes the clip loop forever. Playing a pose and RedM platform notes cover both.
Always wait for HasAnimDictLoaded, and give the wait a timeout
A clip requested from a dictionary that never loaded fails completely silently. No error, no warning in the console — the character simply stands there. The five-second timeout above is the difference between a bug you can read and a bug you have to guess at.
This edit does not survive reinstalling the pack
Keep a copy of poses_client.lua and your manifest change outside the resource folder. Routes 1 and 2 live in your own files and are not affected.
Generating the rows
Ninety-nine entries is well past the point of hand-typing, and hand-typing is exactly where the seven mismatched dictionaries get introduced as bugs. Generate the list from the table instead.
Drop this into a client script inside this resource (Route 3 above), start it, and read the output in the F8 client console:
RegisterCommand('posedump', function()
for i, c in ipairs(Config.Clips) do
print((" { label = '%s', dict = '%s', clip = '%s', duration = -1 },")
:format(c.label, c.dict, c.clip))
end
print(('-- %d entries'):format(#Config.Clips))
end, false)That prints 99 pasteable rows in whatever shape you want — change the format string to emit your emote resource's field names, or a SQL INSERT, or a JSON array. The commented-out rows are absent from the table, so #Config.Clips is 99 and the array is contiguous; you do not have to filter anything.
A generated list must read the table, not build names from ids
Both of the obvious shortcuts are wrong here:
'redmorrow@' .. id .. '@f'breaks on seven rows —regard5,pose1,pose4,hatsdown_clip,pose28,pose47andpose47_pose47all live in a dictionary named after a different pose. Building from theclipfield instead narrows it to six, becausepose47_pose47's clip ispose47; building from neither and reading thedictfield is the only approach that is right 99 times out of 99.- A
for i = 1, 99loop over'pose' .. ibreaks immediately, because the ids are not sequential and not all of them start withpose.
Read c.dict and c.clip off the row. The table is the only authority, which is precisely why it ships readable.
If you would rather not add a script to the pack at all, clips.lua is a 23 KB text file — open it, and a find-and-replace in your editor gets you the same rows.
Every pose is a loop with no root motion
Worth stating plainly, because it decides what these are good for.
All 99 entries are mode = 'loop', all 99 are rootMotion = false, and the durations in clips.lua run from 0.033 s to 89 s — the eight 0.033 s entries are a single frame at 30 fps, which when looped is a static pose held indefinitely. Full breakdown in Durations.
| Suitable for | Not suitable for |
|---|---|
| A pose or photo-mode menu | Action emotes with a beginning and an end |
| Holding a position for a screenshot | Anything that needs the character to walk, turn or travel |
| Standing idles for NPCs and shopkeepers | A gesture that should play once and release |
A 1.967 s loop played as a one-shot is over before the player has let go of the key, which is why nothing in the table ships as once. If you want a pose to stop by itself, run your own timer and clear the task — do not reach for a play-once flag.
The duration field is metadata the pack author wrote. Nothing in this build reads it, and it is not read out of the .ycd. Use it to label or sort your menu; pass -1 to the native and the clip loops regardless. GET_ANIM_DURATION will tell you what the game actually thinks a clip's length is.
Checking a dictionary before you ship a list
One line in the F8 client console tells you whether a dictionary reached the client at all:
DoesAnimDictExist('redmorrow@pose5@f')true means the pack streamed. false means the resource is not started, the ensure line does not match the folder name on disk, or the asset-pack entitlement check failed — dependency '/assetpacks' is a Cfx.re licence-key check, and because this pack genuinely streams files, nothing in the resource can work around it.
The ensure line follows the folder, not the manifest
In CFX the resource name is the directory name. The name 'redmorrow' field in fxmanifest.lua is metadata and renames nothing. The download is rm_100_female_pose.zip, so ensure rm_100_female_pose is correct; if you follow the README and rename the folder to redmorrow, then ensure redmorrow is correct. Either works. The dictionary names baked into the .ycd files are redmorrow@...@f no matter what the folder is called, so your rows never change.
There are no exports in this build, which means — unlike the sibling pack — renaming the folder cannot break anything in your own code. Just keep the two in step. More in Installation.
Next
- Playing a pose — the native call, the dictionary wait, stopping cleanly
- The clips.lua table — every field, and the six commented-out rows
- Animation list — all 99 with dictionary, clip and duration
- Troubleshooting — symptom, cause, fix
- Animation Basics — how two resources fight over one character
- 30+ Female Poses — the sibling pack, which does ship a menu, a command and exports