Coding Standards (CityRP)
Last updated
May be stale
This page was last updated ago, on 3 October 2026. Check what it says against the code before relying on it.
This is the CityRP-specific coding standards gate for humans and agents. General Limelight process and architecture still live in the standards repo. If existing code conflicts with this page, this page wins for new and refactored work — nearby legacy is not permission to copy a forbidden pattern.
| Layer | Role |
|---|---|
.cursor/rules/agent-musts.mdc |
Always-injected agent summary |
| This topic | Authoritative CityRP checklist + reject criteria |
| Modern APIs | Per-category mandatory vs forbidden APIs |
| LuaCATS Annotations | Mandatory function docs |
| AGENTS.md | Silent-failure footguns unique to this codebase |
CI (lint-diff + standards-diff) |
Machine enforcement on changed lines |
Non-negotiables
- Modern APIs only for new/refactored code — see modern-apis.md.
- Every function has a LuaCATS doc comment directly above it (no blank line) — see luacats.md.
- File headers use
--- @namespace, never--- @module. - Lua idioms:
~=/not/and/or— never!=/!/&&/||(GMod accepts the C forms silently;glualintrejects them). Semicolons only to pack multiple statements on one line (if x then y = 1; return y end) — a trailinglocal x = 5;orend;is valid Lua thatglualintwon't flag, so remove any you find. - Client UI goes through
cityrp.ui— see ui-scaling.md — and takes its colours, fonts and patterns from ui-style.md. - Commands: player tools on
/viacityrp.command.add; staff (including RP Assistant) on!viacityrp.acommands.AddCommand; always pass category, syntax, tip — see commands.md. - Async callbacks re-check
IsValidinside the callback — see AGENTS.md footgun #2. - Do not introduce
umsg.*,SendLua(), orBroadcastLua().
Pre-submit checklist
Use this before opening or updating a PR:
- Touched functions have LuaCATS annotations (three dashes, no blank line before
function) - No new
ScrW() / 1920,LimelightHUD.GetScaleFactor(), or fixed UIsurface.CreateFontsizes - No new
"say /..."command literals — usecityrp .../cityrp.command.consoleCommand - No new
--- @moduleheaders - No new
cityrp.item.register/ spawnable inheritance-only bases — useITEM:New - No new NW/NW2 vars for player state — use
SetNSVar/setLocalPlayerVariable - No new
color_white/color_black— useCOL_WHITE/COL_BLACK - No LDoc tags, two-dash
-- @tagannotations, or blank lines between a doc comment and its declaration - New commands have category + syntax + tip on the correct
/or!API - New extension points follow Hooks: a
GM:default for each veto hook, guards beforehook.Run, one lifecycle hook per action - Logging uses
cityrp.logs.add(audit) orcityrp.logging.GetLogger(diagnostics), notprint -
glualintclean on changed lines:python3 .gitea/scripts/lint-diff.py --base origin/master gamemodes/cityrp_2_by_limelightgaming - Standards patterns clean on changed lines:
python3 .gitea/scripts/standards-diff.py --base origin/master
What review / CI will reject
These are treated as standards violations on lines your diff adds or modifies (pre-existing legacy elsewhere in a file is not enough reason to add more of it):
| Pattern on a changed line | Why |
|---|---|
ScrW() / 1920 (or ScrH() / 1080) |
Bypass of cityrp.ui |
LimelightHUD.GetScaleFactor() |
Same — use cityrp.ui (grandfathered only under core/derma/cl_hud_*.lua until migrated) |
--- @module |
LuaLS drops the entire doc comment — use @namespace |
A "say /..." / "say !..." literal, wherever it is built |
Chat-pipeline detour — footgun #13 |
umsg.*, SendLua(, BroadcastLua( |
Banned networking |
A github_pat_… / ghp_… token, or a quoted 36-character mixed-case alphanumeric string, even in a comment |
Committed API token: revoke it, because deleting the line leaves it in history. Read tokens with ENV:Get — see environment.md |
cityrp.item.register( |
Legacy item API — use ITEM:New |
ply:SetNW* / SetNW2* on a player |
NW vars are the avoided tier for player state — use SetNSVar / setLocalPlayerVariable |
color_white / color_black |
Use COL_WHITE / COL_BLACK from sh_enumerations.lua |
LDoc tags (@tparam, @treturn, @classmod, @usage, @number, @local), @vararg |
Not LuaCATS — they render nowhere, on two dashes or three |
Any two-dash -- @tag line, --[[--- @as T]] |
Invisible to LuaLS — annotations need three dashes, inline casts take none |
A file-level function your diff adds with no --- comment above it |
Non-negotiable 2 — see luacats.md |
| A blank line between a doc comment and its declaration | LuaLS never associates them; the annotation is dropped |
cityrp.command.add / cityrp.acommands.AddCommand without category, syntax and tip |
F1 shows "No description." — see commands.md |
The annotation, colour-constant and NW-var rules apply under
gamemodes/cityrp_2_by_limelightgaming/ only: vendored addons are not on the docs site and are
not ours to re-plumb. The doc-comment rules look at file-level declarations, so panel callbacks
and nested helpers are review's job, not CI's.
C-style operators are rejected by glualint via lint-diff.py. Patterns in the table above are
rejected by standards-diff.py. Both run in the Code Linting workflow, which tests its own diff
gates first (test-diff-scripts.py) — these checks fail open when there is nothing to diff, so a
base resolved to the wrong commit would report success without checking anything.
Warnings (do not fail CI)
Three standards-diff.py checks report as a ::warning:: instead of ::error:: and do not fail
the run.
net-entity-unchecked flags a server-side net.Receive handler your diff touches that indexes a
net.ReadEntity() result before checking it with IsValid. A client can name any entity, or
none, so validate a client-named entity before using it. The handler can be an inline function
or one named and defined in the same file. Either of these counts as the check, as long as it
comes before the first ent: or ent. use:
IsValid(ent)orent:IsValid()- passing
entto a validator in anifcondition, such asif not IsCharger(ent) then return end
The rule asks only for validity: class, owner and distance checks depend on what the handler is
for. cl_*.lua files, client/ folders, if CLIENT then blocks and the else of
if SERVER then are exempt. It reads one handler at a time, so aliasing the entity to another
variable, or a handler defined in another file, gets past it. That is review's job.
excessive-prose flags a 4+ line comment block your diff adds, outside
a file-level header, that isn't LuaCATS annotations (footgun #12 — comment only where a reader
would be surprised). It's a warning rather than a hard reject because there's no whitelist for a
genuinely long, legitimate explanation — the same check runs locally as a PostToolUse hook
(.claude/hooks/check-comment-blocks.py) that surfaces the same nudge mid-edit instead of at
review time.
net-write-localplayer flags a net.WriteEntity your diff adds whose argument is
LocalPlayer(), or a local assigned from LocalPlayer() earlier in the same function. The server
already knows who sent a message: it is the second argument to the net.Receive callback,
function(len, ply). A client that writes its own player in invites the handler to read that entity
back and trust whichever player a client names. Drop the write and use the callback's ply instead.
Files that are server realm by name (sv_*.lua, init.lua, or under an sv/ or server/ folder
other than gamemode/core/server/) are exempt.
When you touch legacy code
Editing a file that still uses a forbidden pattern does not require rewriting the whole file. It does require that your new or modified lines follow this page. Prefer migrating the local call site when the change already has to understand that code (especially UI scale).