The Combat Moves Menu
PGUP, or /combatmoves. The panel opens over the game and takes your input while it is up — SetNuiFocus(true, ...) hands the keyboard to the page, so your character stands still until you close it.
Driving it
| Tabs along the top | one per feature — click to jump straight there |
| Search | filters the current page as you type; Tab focuses it |
| Arrow keys | move, and change the value on a slider or options row |
| Enter | select |
| Backspace | go back a page |
| Escape | close |
| Mouse | hover to highlight, click to activate, click a ◀ ▶ to nudge a value, wheel to scroll, click outside to close |
| Back / close | the two buttons top right; Back hides itself on the root page |
Every change applies immediately. Nothing needs reloading and you do not have to close the panel to see the effect.
Menu changes are not saved
They last until the resource restarts, are not written to config.lua, and are not sent to anyone else. To make a value permanent, set it in config.lua. To make one unreachable, use a convar or hide the row.
The pages
| Page | Tab | Subtitle |
|---|---|---|
| Combat Moves | — | the root list |
| Combat Slide | Slide | Hold C while running |
| Combat Roll | Roll | LALT, direction from your movement keys |
| Gun Tricks | Tricks | Spinning / Idle, plus a Settings sub-page |
| Dive, Crawl & Gun | Dive | G to dive, hold INPUT_DUCK to go prone |
The subtitles are built from your actual key settings, so they stay right after you change a key. The values are interpolated raw, with nothing mapping a control name to a friendly word — which is why the dive subtitle reads INPUT_DUCK rather than "crouch".
The root list
| Section | Rows |
|---|---|
| (no heading) | Combat Slide >, Combat Roll >, Gun Tricks >, Dive, Crawl & Gun > |
| Menu | Mouse cursor |
| Pages | Combat Slide, Combat Roll, Gun Tricks, Dive, Crawl & Gun |
| Session | Sliding enabled, Rolling enabled, Close |
The per-feature rows are covered on their own pages: Combat Slide, Combat Roll, Gun Tricks, Dive, Crawl & Gun.
Close is appended after the two Session toggles with no heading of its own in between, so it belongs to that section: its id is menu.session.close, and Config.Menu.sections['menu.session'] = false takes Close away along with the toggles.
The "Sliding enabled" row is a switch, not an indicator
It always reads as on, whatever state sliding is actually in, because its getter is hard-coded. Toggling it does call SetEnabled and does work — the tick just does not follow. There is no way to read the real state from outside either: SetEnabled writes a module-local enabled flag that has no getter and no export. IsSliding() — and cs_watch, which prints the same export — only tells you whether a slide is in progress at that moment.
The mouse cursor
Config.MenuMouse ships true. It is the cursor half of SetNuiFocus(true, hasCursor).
With it false there is no cursor at all, so the panel's hover and click handlers exist but can never fire — the menu becomes keyboard-only.
The trade-off
A cursor takes mouse look away from the player for as long as the panel is up. The keyboard bindings keep working either way, so a server that would rather players kept their camera can turn it off:
Config.MenuMouse = falseCombat Moves → Menu → Mouse cursor toggles it live, and it applies straight away rather than on the next open.
Choosing what the menu shows
Config.Menu gates the panel at four levels, coarse to fine.
Hiding something does not disable it
It only takes it off the panel — the feature keeps running on whatever config.lua says. To turn a feature off, use its own enabled flag. See Server setup.
Config.Menu.enabled | the master switch. false and the menu never opens: the key is ignored, /combatmoves says so once, and the OpenMenu export returns false |
Config.Menu.pages | a whole page. It loses its row on the front list and its tab, and the strip closes up rather than leaving a gap |
Config.Menu.sections | a grey heading inside a page, and every row under it down to the next heading |
Config.Menu.options | one row |
Ids are page.section.row, lower case with dashes for spaces. A row above the first heading has no section part.
pages and sections list every one there is, so they read as a contents page. options is the opposite — anything absent is shown, so it only needs the rows you want gone:
Config.Menu.pages.tricks = false -- no Gun Tricks page
Config.Menu.sections['dive.dive-camera'] = false -- no dive camera block
Config.Menu.options['slide.behaviour.mud-decal'] = false -- just that one rowEvery section id
| Page | Sections |
|---|---|
slide | slide.behaviour, slide.combat, slide.camera |
roll | roll.try-it |
tricks | tricks.tricks, tricks.more |
dive | dive.diving, dive.crawling, dive.prone-camera, dive.dive-camera, dive.stealth, dive.fix |
| the root list | menu.menu, menu.pages, menu.session |
Find ids with cs_menuids, not by hand
Do not derive an id yourself
cs_menuidsIt prints every id the menu can switch with its current state, read off the pages as they are really built — so what it prints is exactly what config.lua will match. Two filters:
cs_menuids dive | only ids with dive in them. It is a substring match anywhere in the id, not a prefix, so root-list ids such as menu.dive-crawl-gun and rows such as dive.diving.invincible-mid-dive come through too |
cs_menuids off | only what is currently hidden |
The ids are generated from your headings and labels by the same code path the live menu uses, so the listing and the menu cannot disagree.
The GetMenuIds() export returns the same data as a table.
Behaviour worth knowing
- A heading left with nothing under it is dropped, so hiding rows one at a time never strands a title.
- Hidden rows are not reachable by id either — the front end cannot drive a row that was filtered out.
- No tab number is written down anywhere. The live index is worked out from the tabs that are on, so hiding a page cannot leave a tab highlighting the wrong thing.
/guntrickschecks the page switch as well as the master one, so it is not a way round a hidden Tricks page.
The Pages section, and locking it
The root list's Pages section writes to Config.Menu.pages live. It is the way back if a player switches all four pages off — no page switch can hide it.
To lock the page set to whatever config.lua says:
Config.Menu.sections['menu.pages'] = falseThat is the only way back
With menu.pages hidden, nothing in game can turn a page on again. It is what a server wants, but set your pages deliberately before you hide it.
Common setups
Read-only panel. Hide the sections that contain settings and leave the action rows:
Config.Menu.sections = {
['slide.behaviour'] = false,
['slide.combat'] = false,
['slide.camera'] = false,
['dive.diving'] = false,
['dive.crawling'] = false,
}No menu at all.
Config.Menu.enabled = falseEvery feature still works on your values.
Only two of the four features visible.
Config.Menu.pages = { slide = true, roll = true, tricks = false, dive = false }The tabs close up — no gap, no dead tab.
The front end
Three plain files in nui/ plus the artwork in nui/ASSETS/. No frameworks, no CDN requests and no webfont requests, so it works on a server with no outbound internet.
It is styled after the RDR2 in-game panels using that artwork: modal_bg.png for the torn black plate, item_bg.png and tab-item.png for the painted strips, border.png inverted for the header buttons, and the two Rockstar faces — RDR Catalogue for headings and row names, RDRLino for body copy. click.mp3, toggle.mp3 and turn-on.mp3 are wired to selection, value changes and opening.
Anything the page loads must be listed in files{}
If it is not in fxmanifest.lua, RedM will not serve it and the panel falls back to unstyled text. The globs for nui/ASSETS/best/*.png, nui/ASSETS/*.ttf and nui/ASSETS/*.mp3 are there for exactly that reason — if you add artwork, add it to the manifest. Note that every sprite the panel draws is referenced as ASSETS/best/... in style.css, so the live set is the one in nui/ASSETS/best/; the plain nui/ASSETS/*.png glob serves only the originals kept for comparison, and replacing art there changes nothing on screen. The fonts and the sounds do sit directly in nui/ASSETS/.
Renaming the resource
The NUI fetch origin is https://rm_combat_moves/<callback>, built from the RESOURCE constant in nui/app.js. If you rename the folder, that constant has to change too or the panel will open and then do nothing. The full rename checklist is on Developer API.
Exports
exports.rm_combat_moves:OpenMenu() -- false if disabled, or it was open (it closes)
exports.rm_combat_moves:CloseMenu()
exports.rm_combat_moves:IsMenuOpen()
exports.rm_combat_moves:IsMenuEnabled()
exports.rm_combat_moves:SetMenuEnabled(false) -- closes it if it is up
exports.rm_combat_moves:ForceCloseMenu() -- recovery; releases NUI focus unconditionally
exports.rm_combat_moves:GetMenuIds()ForceCloseMenu() is the one to call from a jail or cutscene script if you need to be certain the player does not keep NUI focus.