Chat Commands (`/` vs `!`)
Last updated
May be stale
This page was last updated ago, on 26 August 2026. Check what it says against the code before relying on it.
CityRP has two separate command systems. Choosing the wrong one "works" at runtime (access flags still gate execution) but lands the tool under the wrong chat prefix, the wrong F1 grouping conventions, and breaks the standardised split from the acommands migration.
| Kind | Prefix | Names players use | Registration API | Who it's for |
|---|---|---|---|---|
| Normal / slash-commands | / |
/help, /showid |
cityrp.command.add |
Non-privileged player RP/job tools. Generally available to all players (job checks may still apply inside the callback). |
| Privileged / staff / admin commands | ! |
!place, !tr |
cityrp.acommands.AddCommand |
Anything that requires staff privileges — including RP Assistant. |
If a command needs staff privileges of any kind, use cityrp.acommands.AddCommand. Do not
register it with cityrp.command.add and a restricted access flag. RP Assistant (+ access /
"assistant" category) is still a privileged command and belongs on !.
See also the Modern APIs rows for both systems, Coding Standards, and AGENTS.md footgun #14.
Metadata is mandatory
Every command — / and ! — must ship with:
category— a key fromcityrp.command.categoryAliases(e.g."roleplay","government","assistant","moderator"). Controls F1 grouping.help/ syntax — argument shape such as"<player> <amount>", or"<none>"when there are no arguments. Shown in invalid-syntax / missing-argument error notifications.tip— a short, clear description of what the command does. Shown in the F1 commands menu. Omitting it yields"No description."in F1.
Write tips as a short sentence players/staff can scan: what it does, not how it's implemented.
Examples from the codebase: "Open or close the Nexus garage door.",
"Send a help-ticket response to a player.", "To go to the given place".
Signatures
Player commands (/)
cityrp.command.add(name, access, minArgs, callback, category, syntax, tip[, emgr])
- Callback is
(ply, arguments)—argumentsis a string table. - Access
"b"is the usual "any player" flag for non-privileged tools.
cityrp.command.add(
"nexusgarage",
"b",
0,
function(ply)
-- ...
end,
"government",
"<none>",
"Open or close the Nexus garage door."
)
Staff commands (!)
cityrp.acommands.AddCommand(name, func, minArgs, access, emgr, category, syntax, tip)
- Callback is
(ply, ...)— varargs, not a table. - Parameter order differs from
cityrp.command.add(notablyemgrbeforecategory).
cityrp.acommands.AddCommand("tr", cityrp.acommands.helpreply, 2, "+", false, "assistant",
"<player> <message>",
"Send a help-ticket response to a player."
)
Moving a staff tool off /
When converting a staff tool from / to !, leave a redirect so old muscle memory fails
loudly instead of silently:
cityrp.command.redirectToAcommand("place") -- /place → "use !place"
cityrp.command.redirectToAcommand("warp", "place") -- /warp → "use !place"
Redirects notify the player and are excluded from F1.
Helpers
Prefer these over copy-pasted lookup/parsing:
cityrp.player.resolveTarget/cityrp.acommands.GetPlayerObjectfor player targetscityrp.command.parseMoneyfor money amounts
Invoking from code
Do not dispatch via say /cmd. From clientside UI use
LocalPlayer():ConCommand("cityrp <cmd> <args>"); from serverside, for a / command, prefer
cityrp.command.consoleCommand(ply, cmd, argsTable). See AGENTS.md
footgun #13 and the Modern APIs "Running a CityRP command from code" row.
cityrp.command.consoleCommand only knows the / registry (cityrp.command.stored) — it
does nothing useful for a ! command, and will silently hit a leftover redirect stub
(cityrp.command.redirectToAcommand) if the command used to live on /. The equivalent
serverside dispatch for a ! command is cityrp.acommands.HandleCommand(ply, label, argsTable, "") (argsTable[1] is the command name, same convention as consoleCommand's argsTable;
label and the trailing string are unused by the handler). If code building a dispatch table
does not control which registry the target command is on — e.g. a shared picker menu that can
open for either a / or a ! command — check cityrp.command.stored[cmd] first (and skip it
when .redirect is set, since that's just the deprecation stub left behind by a / → !
move) before falling back to cityrp.acommands.commands[cmd]. See
core/plugins/itemgrantmenu/sv_init.lua's net.Receive(PLUGIN.Net.Menu, ...) for a worked
example — it backs !giveitem, !takeitem and !refund (all ! commands today) through the
same submit handler, and still has to handle both registries since nothing stops a future
command it opens for from living on /.