Skip to content

Combat Moves Developer API ​

Everything here is client-side. There is no server script.

The resource name is the folder name

Call these as exports.rm_combat_moves:Name(...). If you rename the folder, every call site has to change — see Renaming the resource.

Combat Slide ​

ExportReturnsNotes
IsSliding()boolean
Slide()booleanstarts one; false if it was refused
StopSlide()—ends one in progress
SetEnabled(state)—false stops the key being read

Combat Roll ​

ExportReturnsNotes
IsRolling()boolean
Roll()booleandirection comes from the keys held at the time
SetRollEnabled(state)—

Gun Tricks ​

ExportReturnsNotes
IsSpinning()boolean
ToggleGunTrick(randomStart)booleanpass true to start on a random trick; false back if the spin was refused
StopGunTrick()—
NextGunTrick()numberthe new trick index
SetGunTrick(which)booleanindex 1–6, or the trick's name; false if neither matches
GetCurrentGunTrick()number
GetGunTricks()tablesee below
SetGunTrickHands(mode)boolean'auto', 'dual', 'single', 'left'; false for anything else
GetGunTrickHands()string
SetGunTricksEnabled(state)—
ForceResetGunTricks()—recovery; clears state unconditionally
DumpGunTrickPrompts()—prints prompt diagnostics

GetGunTricks() returns an array of:

lua
{ index = 1, name = 'Reverse Spin', desc = 'Twirl your guns',
  emote = 'KIT_EMOTE_TWIRL_GUN_VAR_A', current = true }

There is also a key field, but only when the trick has a slot bound to it. Config.GunTricks.slots and Config.GunTricks.nativeSlots both ship empty, so on a default install the lookup returns nothing and key is absent from every entry.

Dive, Crawl & Gun ​

ExportReturnsNotes
IsDiving()booleantrue while airborne or down
GetDiveState()string'idle', 'launch', 'crawl', 'aim', 'onback', 'sit'
GetDiveWeaponClass()string'unarmed', 'pistol', 'rifle', 'none' — which animation set applies
Dive()booleanfalse if not idle
GoProne()boolean
GetUpFromDive()—
StopDive()—recovery; gets you up from any state
IsStealthOn()boolean
SetStealth(on)—bypasses the double-tap
SetDiveCameraPreview(which)—'prone', 'dive', or nil to clear
GetDiveCameraPreview()string or nil
SetDiveEnabled(state)—
IsDiveEnabled()boolean
ExportReturnsNotes
OpenMenu()booleanfalse if the menu is disabled, or it was already open (it closes)
CloseMenu()—
IsMenuOpen()boolean
IsMenuEnabled()booleanreflects Config.Menu.enabled
SetMenuEnabled(on)booleancloses the menu if switching off while open
ForceCloseMenu()—recovery; releases NUI focus unconditionally
GetMenuIds()tableevery id Config.Menu can gate

GetMenuIds() returns an array of:

lua
{ kind = 'option', id = 'slide.behaviour.mud-decal',
  label = 'Mud decal', page = 'slide', on = true }

kind is 'master', 'page', 'section' or 'option'. This is what cs_menuids prints — it walks the pages as they are really built, so the ids cannot drift from the ones the menu actually uses. A page that throws is caught rather than taking the listing with it, which matters because the whole point of it is to be usable when something is already wrong.

Events ​

Four net events the resource listens on. It does not emit any. Each only affects the client that receives it.

lua
TriggerClientEvent('combat_slide:setEnabled',          src, false)
TriggerClientEvent('combat_slide:setRollEnabled',      src, false)
TriggerClientEvent('combat_slide:setGunTricksEnabled', src, false)
TriggerClientEvent('combat_slide:setDiveEnabled',      src, false)

The event names keep the old combat_slide: prefix

From earlier versions, so existing integrations keep working. They are plain strings and nothing ties them to the resource name — renaming the resource does not change them.

Convars ​

Read freshly on every slide — readPolicy() is called inside runSlide(), not at start-up — so a setr change takes effect on the player's next slide with no restart.

ConvarTypeDefault
combat_slide_allow_timescalebooltrue
combat_slide_allow_deadeyebooltrue
combat_slide_allow_infinitebooltrue
combat_slide_max_durationnumber (ms)10000

They do not clamp the menu. The Duration slider still offers its full range and the Dead Eye and bullet time rows still appear and toggle; the limit is applied when the slide runs. There is no infinite-slide row in the menu at all — Config.InfiniteSlide is a config.lua setting only. They also cover the slide feature only — roll, gun tricks and dive never read them, and the dive's own bullet time (Config.Dive.enableBulletTimeDuringDive) has no convar over it.

Mutual exclusion ​

The four features interlock, but the checks are one-directional rather than mutual. If you add a fifth move, know which way each guard points or the animations will fight.

  • A roll refuses to start while a slide is running
  • A gun trick refuses to start while a slide or a roll is running, and a running trick is cut if either starts
  • Diving stands down while any of the three is running
  • A slide can start during a roll or a gun trick: canSlide tests only its own sliding flag and never calls IsRolling() or IsSpinning()

The check the dive uses internally:

lua
local function otherMoveActive()
    local x = exports.rm_combat_moves
    return x:IsSpinning() or x:IsSliding() or x:IsRolling()
end

Examples ​

Disable moves in a safe zone ​

lua
-- client, in your own resource
local inSafeZone = false

CreateThread(function()
    while true do
        Wait(1000)
        local now = isPlayerInSafeZone()
        if now ~= inSafeZone then
            inSafeZone = now
            exports.rm_combat_moves:SetEnabled(not now)
            exports.rm_combat_moves:SetRollEnabled(not now)
            exports.rm_combat_moves:SetDiveEnabled(not now)
            exports.rm_combat_moves:SetGunTricksEnabled(not now)
        end
    end
end)

Block moves while an animation of yours plays ​

lua
local function busy()
    return exports.rm_combat_moves:IsSliding()
        or exports.rm_combat_moves:IsRolling()
        or exports.rm_combat_moves:IsSpinning()
        or exports.rm_combat_moves:GetDiveState() ~= 'idle'
end

if not busy() then
    playMyAnimation()
end

A jail or restraint state ​

lua
exports.rm_combat_moves:StopSlide()
exports.rm_combat_moves:StopDive()
exports.rm_combat_moves:StopGunTrick()
exports.rm_combat_moves:SetMenuEnabled(false)

ForceCloseMenu() as well if you need to be certain the player does not keep NUI focus.

Stealth on entering a hideout ​

lua
exports.rm_combat_moves:SetStealth(true)

This bypasses the double-tap entirely, so it is the right way to drive stealth from your own resource.

Layout ​

config.lua              every setting, shared script
client/natives.lua      native wrappers — the CS.* table
client/keys.lua         key name to control hash
client/input.lua        reads the slide key in one of three input modes
client/slide.lua        Combat Slide
client/roll.lua         Combat Roll
client/guntricks.lua    Gun Tricks
client/dive.lua         Dive, Crawl & Gun
client/menu.lua         the menu: pages, tabs, NUI bridge
client/debug.lua        console commands
nui/index.html          the panel
nui/style.css
nui/app.js
nui/ASSETS/             sprites, two typefaces, three sounds

Load order in fxmanifest.lua matters: natives and keys first, then the four features, then menu (which reads their exports), then debug.

client/natives.lua ​

natives.lua defines 140 CS.* wrappers, and most of the natives the resource calls go through one of them. A handful are called flat instead: dive.lua calls StartExpensiveSynchronousShapeTestLosProbe directly, and both slide.lua and dive.lua call RequestAnimDict, HasAnimDictLoaded, DisableControlAction, IsPedRagdoll and IsPedOnMount directly. Two reasons the wrapper table is worth knowing if you extend the resource.

Pointer parameters crash ​

A native declared as taking Any* or Ped* wants a pointer to a struct or handle, not the value. Calling one flat through Citizen.InvokeNative does not fail gracefully — it throws a native exception and takes the frame down.

Check the params type before wrapping anything with an Any*

Unless it is an output slot. CS.PlayAmbientSpeech is the one case of this in natives.lua: it is a documented no-op because its parameter is an eight-field struct that cannot be built from Lua at all, and the comment above it records the layout.

Several natives report nonsense on this build ​

NativeObserved
IS_PED_WEAPON_READY_TO_SHOOTfalse while stood still, aiming, trigger down and the gun firing
IS_PED_SHOOTINGnever observed true
IS_PLAYER_FREE_AIMINGfalse under assisted aim even with aim held — gate on the aim control too
IS_PED_INCAPACITATEDfired mid-slide on a player at full health

Use ammo deltas (GET_AMMO_IN_PED_WEAPON) to tell whether a shot came out. That is what the debug report does.

Also worth knowing: RDR2 hides a prompt whose control is disabled. If you suppress a control to stop it doing its normal job, its prompt row stops drawing too. The number keys 1–6 are INPUT_SELECT_QUICKSELECT_* controls, which the game disables during an emote, so their prompt rows never draw anyway.

client/menu.lua ​

A page is a function returning { title, key, tabKey, subtitle, items }. Items carry their own onSelect / onChange, so there is no id switch anywhere. Pages are pushed on a stack; backspace pops it.

Adding a page:

  1. Write pageMine = function() return { title = ..., key = 'mine', tabKey = 'mine', items = { ... } } end

  2. Add it to the forward declaration at the top of the file.

  3. Add { key = 'mine', label = 'Mine', title = 'My Feature', page = pageMine } to ALL_TABS.

  4. Add a row to pageRoot with pageKey = 'mine'.

  5. Add mine = true to Config.Menu.pages.

Do not write a tab index anywhere

show() derives the live index from whichever tabs are enabled, so hiding a page renumbers the rest correctly. The ids Config.Menu gates are generated from your headings and labels by walkItems, which is also what GetMenuIds uses — one code path, so the listing and the live menu cannot disagree.

NUI contract ​

Lua sends three actions — open, close, tricks. The page posts five callbacks — select, change, tab, back, close. The fetch origin is https://rm_combat_moves/<callback>, built from the RESOURCE constant in app.js, which must equal the folder name.

Renaming the resource ​

The folder name is the resource name. If you rename it:

  1. exports.rm_combat_moves: — every call site

  2. RESOURCE in nui/app.js — the NUI fetch origin

  3. name in fxmanifest.lua

  4. ensure in server.cfg

The net events, convars, key mapping names and console prefixes are plain strings and do not need changing — renaming them would break an existing server.cfg for nothing.

Extending it ​

The resource is escrowed by default, so the feature files are encrypted in a standard build. fxmanifest.lua carries no escrow_ignore block — that is the only thing that exempts a file from asset escrow — so nothing in the shipped manifest keeps config.lua or client/keys.lua readable either. A source edition is available with every file unencrypted; see the licence that ships with it for what that permits.

If you are working from the source edition, two habits are worth keeping:

Measure before you change an animation value. cs_diveanim and cs_anim print each clip's real duration; cs_flags decodes flag bits by name. Most animation problems in this resource turned out to be a timing or flag value that could be read rather than guessed.

Keep the mutual-exclusion rule. Any new move should check otherMoveActive() and stand down, and should be added to the three existing features' checks if they must stand down for it too.

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