Skip to content

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 ​

FileWhat it is
fxmanifest.luafx_version 'cerulean', game 'rdr3', lua54 'yes', dependency '/assetpacks'
clips.luaA data table and nothing else. Sets Config.Clips — 105 rows, 99 live
server.luaOne line, printing [redmorrow] Female animation pack started.
stream/104 .ycd dictionaries, 452 KB total
README.mdStale, 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' with rootMotion = false. All 99. There are no once or hold rows, 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:

OptionWhat it means
Copy itPaste the rows into your own resource, or copy clips.lua into it and load it from your own manifest
Add to this onePut 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 ​

RouteEffortSurvives a pack updateGood when
An emote resourceLowYesYou already run one, and players already know how to open it
Your own menu or commandMediumYesYou want poses behind a job, a prop, or a photo mode
A client script inside the packLowNoYou 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:

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

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

A command over that index:

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

lua
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)
end

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

FieldReality
labelThe raw id for almost every row — pose67, pose68, redmorrow_p1. Only the seven regard entries are capitalised, as Regard1 and so on
categoryAlso the id, per row. Grouping by category gives you 99 groups of one
idNot 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.

  1. Write your script next to clips.lua — call it poses_client.lua:

    lua
    local 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)
  2. Add it to the manifest, after clips.lua:

    lua
    client_scripts {
        'clips.lua',
        'poses_client.lua',
    }

    The order matters. client_scripts load in the order listed, and Config.Clips does not exist until clips.lua has run.

  3. Restart the resource. /pose pose5, /pose 1 and /pose stop now 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:

lua
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, pose47 and pose47_pose47 all live in a dictionary named after a different pose. Building from the clip field instead narrows it to six, because pose47_pose47's clip is pose47; building from neither and reading the dict field is the only approach that is right 99 times out of 99.
  • A for i = 1, 99 loop over 'pose' .. i breaks immediately, because the ids are not sequential and not all of them start with pose.

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 forNot suitable for
A pose or photo-mode menuAction emotes with a beginning and an end
Holding a position for a screenshotAnything that needs the character to walk, turn or travel
Standing idles for NPCs and shopkeepersA 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:

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

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