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
| Export | Returns | Notes |
|---|---|---|
IsSliding() | boolean | |
Slide() | boolean | starts one; false if it was refused |
StopSlide() | — | ends one in progress |
SetEnabled(state) | — | false stops the key being read |
Combat Roll
| Export | Returns | Notes |
|---|---|---|
IsRolling() | boolean | |
Roll() | boolean | direction comes from the keys held at the time |
SetRollEnabled(state) | — |
Gun Tricks
| Export | Returns | Notes |
|---|---|---|
IsSpinning() | boolean | |
ToggleGunTrick(randomStart) | boolean | pass true to start on a random trick; false back if the spin was refused |
StopGunTrick() | — | |
NextGunTrick() | number | the new trick index |
SetGunTrick(which) | boolean | index 1–6, or the trick's name; false if neither matches |
GetCurrentGunTrick() | number | |
GetGunTricks() | table | see 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:
{ 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
| Export | Returns | Notes |
|---|---|---|
IsDiving() | boolean | true while airborne or down |
GetDiveState() | string | 'idle', 'launch', 'crawl', 'aim', 'onback', 'sit' |
GetDiveWeaponClass() | string | 'unarmed', 'pistol', 'rifle', 'none' — which animation set applies |
Dive() | boolean | false 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 |
Menu
| Export | Returns | Notes |
|---|---|---|
OpenMenu() | boolean | false if the menu is disabled, or it was already open (it closes) |
CloseMenu() | — | |
IsMenuOpen() | boolean | |
IsMenuEnabled() | boolean | reflects Config.Menu.enabled |
SetMenuEnabled(on) | boolean | closes the menu if switching off while open |
ForceCloseMenu() | — | recovery; releases NUI focus unconditionally |
GetMenuIds() | table | every id Config.Menu can gate |
GetMenuIds() returns an array of:
{ 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.
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.
| Convar | Type | Default |
|---|---|---|
combat_slide_allow_timescale | bool | true |
combat_slide_allow_deadeye | bool | true |
combat_slide_allow_infinite | bool | true |
combat_slide_max_duration | number (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:
canSlidetests only its ownslidingflag and never callsIsRolling()orIsSpinning()
The check the dive uses internally:
local function otherMoveActive()
local x = exports.rm_combat_moves
return x:IsSpinning() or x:IsSliding() or x:IsRolling()
endExamples
Disable moves in a safe zone
-- 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
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()
endA jail or restraint state
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
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 soundsLoad 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
| Native | Observed |
|---|---|
IS_PED_WEAPON_READY_TO_SHOOT | false while stood still, aiming, trigger down and the gun firing |
IS_PED_SHOOTING | never observed true |
IS_PLAYER_FREE_AIMING | false under assisted aim even with aim held — gate on the aim control too |
IS_PED_INCAPACITATED | fired 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:
Write
pageMine = function() return { title = ..., key = 'mine', tabKey = 'mine', items = { ... } } endAdd it to the forward declaration at the top of the file.
Add
{ key = 'mine', label = 'Mine', title = 'My Feature', page = pageMine }toALL_TABS.Add a row to
pageRootwithpageKey = 'mine'.Add
mine = truetoConfig.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:
exports.rm_combat_moves:— every call siteRESOURCEinnui/app.js— the NUI fetch originnameinfxmanifest.luaensureinserver.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.