CityRP Documentation Repository

Chat Commands (`/` vs `!`)

Last updated

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:

  1. category — a key from cityrp.command.categoryAliases (e.g. "roleplay", "government", "assistant", "moderator"). Controls F1 grouping.
  2. help / syntax — argument shape such as "<player> <amount>", or "<none>" when there are no arguments. Shown in invalid-syntax / missing-argument error notifications.
  3. 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) — arguments is 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 (notably emgr before category).
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.GetPlayerObject for player targets
  • cityrp.command.parseMoney for 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 /.