Modern APIs
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.
CityRP is a mature codebase containing many legacy patterns and deprecated systems. The table below is the authoritative, up-to-date list of which API to use for each category. Legacy alternatives remain in the codebase — because they predate the migration, not because they're still correct — and must not be used in new or refactored code.
Agents also see a condensed always-on copy of this table in .cursor/rules/agent-musts.mdc. When you add or change a row here, update that MUST card in the same change. The checklist and CI reject criteria for forbidden patterns are in Coding Standards.
| Category | ✅ Modern API (MANDATORY) | ❌ Legacy / Standard GMod (FORBIDDEN) | Why? |
|---|---|---|---|
| Audit Logging (player/admin actions, transactions) | cityrp.logs.add(nick, steamid, ...) |
print()<br>ServerLog() |
Logs to database and admin interfaces for moderation/audit; print is invisible to admins. |
| Diagnostic Logging (developer debugging, traces) | cityrp.logging.GetLogger(namespace):Debug/Info/Error(...) |
print()<br>ServerLog() |
Namespaced, level-filterable developer diagnostics. Does not replace cityrp.logs.add() for audit events. See logging.md. |
| Items | ITEM:New("base_id") (Fluent), or ITEM:New() for no base |
cityrp.item.register({}) |
Metatable-based fluent API with better inheritance and cleaner code, and it doesn't require a spawnable/purchasable base item to be registered just to be inherited from. See items.md. |
| Networking | See tiers below | umsg.*, SendLua(), BroadcastLua() (banned) |
Pick the highest tier that fits; cityrp.network exists as a library but isn't mandatory yet, so legacy networking (NSVar/LocalPlayerVar) remains the correct default; never use the banned tier. |
| Color Constants | COL_WHITE, COL_BLACK, COL_RED, COL_GREEN, COL_BLUE, COL_GREY (from sh_enumerations.lua) |
color_white, color_black (GMod globals) |
Consistency, easier global theming later, reduces reliance on GMod globals. A colour that has a named role in ui-style.md (panel surfaces, text, brand, information, success, warning, danger, HUD chip) is used by its constant, never retyped. A genuine one-off with no role there is still fine as an inline Color(...) call. |
| Running a CityRP command from code | ply:ConCommand("cityrp <cmd> <args>") (clientside UI)<br>cityrp.command.consoleCommand(ply, cmd, argsTable) (serverside) |
ply:ConCommand("say /<cmd> <args>") |
Both end up in cityrp.command.consoleCommand; say / only adds the chat pipeline in front of it, where a PlayerSay hook can swallow the message, cityrp.replacecodes rewrites %/$q codes inside the arguments, >452 characters are dropped silently, and the arguments are re-quoted by a naive scanner. See AGENTS.md footgun #13. |
| Reaching a plugin you declared a dependency on | PLUGIN.Dependencies["<key>"] |
cityrp.plugin.get("<key>") when the key is already in your .meta.lua |
The loader resolved it for you. A required dependency is never nil — your plugin would not have loaded without it — so there is nothing to guard, which no lookup-plus-guard can match. An optional one may be nil and must be guarded. See plugins.md. |
| "Is this plugin here?" (guarding a cross-plugin call) | cityrp.plugin.IsAvailable("<key>") |
cityrp.plugin.get("<key>") ~= nil; unguarded cross-plugin calls |
get only sees plugins that called register, and ~27 loaded plugins never do — so it reports them missing. IsAvailable reads the loader's bookkeeping, is realm-accurate, and answers false for every reason a plugin can be absent. Use cityrp.plugin.GetDependencyStatus(key) when you need to know which reason; it is for diagnostics, not for guards. See plugins.md. |
| Plugin lookup (you need the plugin's table) | cityrp.plugin.get("<key>") — the folder key, as used by .meta.lua and load order. Inside a plugin, cityrp.plugin.get(PLUGIN.key) |
cityrp.plugin.get("Display Name"); indexing cityrp.plugin.stored directly |
cityrp.plugin.stored is keyed by the plugin key. A display name still resolves through cityrp.plugin.names and costs the same, but warns once per name — the two identities are not derivable from each other (area registers as "Area Manager"). Existing display-name call sites work; convert one when you are already editing that code. See plugins.md and AGENTS.md footgun #22. |
| Exposing an extension point (letting other code allow, refuse or react to your system's actions) | A GM:Can<Verb><Object> default rule, asked through hook.Run after the caller's guards; a GM:<Object><Verb-ed> lifecycle hook fired after each action |
A rule written inline in the plugin behind an ad-hoc hook.Run; one *Changed hook with an action argument; one hook per route to the same outcome |
A hook.Add callback overrides the GM: default, so the default is the rule an addon extends; guards keep staff and kill switches out of an addon's reach; one hook per outcome means a listener cannot miss a route. See hooks.md and ADR 0003. |
| Console Entity | CONSOLE |
console (lowercase) |
console is a deprecated alias kept only for backwards compatibility with external plugins — see admin/sv_init.lua. |
Player chat commands (/ prefix; aka normal / slash-commands) |
cityrp.command.add(name, access, minArgs, callback, category, syntax, tip[, emgr]) |
Raw PlayerSay / concommand parsers for gamemode commands; staff tools registered as / |
Non-privileged player RP/job tools only — generally available to all players. Always pass category (key from categoryAliases), syntax ("<none>" if none; shown on invalid-syntax errors), and a short tip description for the F1 commands menu. Callback is (ply, arguments) table. See commands.md and AGENTS.md footgun #14. |
Staff chat commands (! prefix; aka privileged / admin / staff commands) |
cityrp.acommands.AddCommand(name, func, minArgs, access, emgr, category, syntax, tip) |
Staff tools on / (even with a restricted access flag); ad-hoc PlayerSay handlers for ! |
Always use this if the command requires staff privileges of any kind — including RP Assistant. Same mandatory category/syntax/tip metadata as player commands. Callback is (ply, ...) varargs. When moving a staff tool from / to !, leave cityrp.command.redirectToAcommand(oldName) so /old only notifies to use !old. Prefer cityrp.player.resolveTarget and cityrp.command.parseMoney over copy-pasted lookup/money parsing. See commands.md and AGENTS.md footgun #14. |
UI Scaling (Derma panels, entity menus, HUD overlays, custom Paint) |
cityrp.ui.Scale(), cityrp.ui.GetScaleFactor(), cityrp.ui.CreateScaledFont(), cityrp.ui.OnScaleChanged() |
Ad-hoc ScrW() / 1920 scaling; calling LimelightHUD.GetScaleFactor() directly; fixed surface.CreateFont sizes; hardcoded pixel dimensions that never rebuild on scale change; calling your builder yourself as well as registering it |
One resolution- and preference-aware scale for all UI; fonts and layout stay in sync when cl_llhud_scale or screen size changes. OnScaleChanged runs the callback once at registration, so one registration builds and rebuilds — the callback must be safe to run at registration time. See ui-scaling.md and AGENTS.md footgun #15. |
If existing code conflicts with this table, follow this table — usage of a legacy pattern elsewhere in the codebase is not evidence it's the correct approach, only that it predates this list.
Networking tiers
Networking isn't a simple modern/legacy split — use the highest tier that fits the use case, don't default to a lower one out of habit.
- Top tier: Entity Networking (DTVars).
Entity:NetworkVar(...)/ DataTables (SetupDataTables). Use for anything that's naturally a property of a Lua-authored entity (a SENT or SWEP you define yourself) — it's predicted, delta-compressed, and needs no manual send/receive code. This does not work for GMod's engine-defined vehicle base classes (prop_vehicle_jeep,prop_vehicle_prisoner_pod, etc.) — they don't runSetupDataTablesthe way a Lua SENT does, so DTVars aren't an option for vehicle state. See tier 4. cityrp.network.cityrp.network.Register(...)(core/libraries/sh_network.lua, see networking.md). Covers anything tier 1 doesn't: non-player entity state (doors, vehicles, signs), Global-scope values, LocalPlayer-private state, or any value that needs recipient filtering (:Filter(),:SendPVS()/:SendPAS()), a change hook, or pull-on-visible resync. Not yet mandatory — see Migration status below before reaching for this tier over tier 3. See networking.md for the full builder-chain reference.- Legacy networking. For events/actions, use
net.*directly. For player state that isn't a natural entity property:ply:SetNSVar(name, value)/ply:GetNSVar(name, default)— for state every client needs to know about that player (broadcast to all clients vianet.Broadcast()).cityrp.player.setLocalPlayerVariable(ply, CLASS_TYPE, "_Name", value)— for state only the owning client needs (sent privately vianet.Send(ply)).- Both require the variable name to be pre-registered (see
networking/sh_init.lua) — setting an unregistered name fails silently, with only aprint()to console, not an error. Register the name first. - Both are Player-metatable-only (
FindMetaTable("Player")) — they don't generalize to vehicles or other non-player entities. For non-player entity state,net.*directly is the current option (tier 2 isn't mandatory yet — see below). - Not deprecated: this tier works, is still reasonably efficient, and is the correct default today. It just sits below
cityrp.networkonce that tier becomes mandatory.
- Avoided: low-tier networking.
SetNWInt/SetNW2Int/etc. (NW/NW2 vars). Avoid for player data — don't use it in new or refactored code, see problems/03 and improvements/01. Exception: vehicles currently use this tier for real (sv_vehicle.lua'slocked/engine_on/HPstate) precisely because neither tier 1 (DTVars — engine entity, noSetupDataTables) nor tier 3 (NSVar/LocalPlayerVar — Player-only) apply to them, and they haven't been migrated to tier 2 yet. Until that migration lands, this is the practically justified option for vehicles, not just legacy inertia. - Banned.
umsg.*(pre-netlegacy usermessage library — no structure or size guaranteesnetprovides),SendLua(),BroadcastLua()(execute arbitrary Lua source on the client — a code-injection vector if the string is ever built from anything less than a fully trusted, static literal, and pure legacy debt even when it isn't). Never use these.
Migration status
cityrp.network is live and call sites have started moving onto it — cityrp_Job and _Food (core/libraries/sh_network_vars.lua), the vehicles plugin (core/plugins/vehicles/sh_network_vars.lua), and the player limits registered by cityrp.limits — but it is not yet mandatory for new or refactored code. SetNSVar/GetNSVar, cityrp.player.setLocalPlayerVariable/Player:SetLPlyVar, and SetNW* for vehicles all remain correct APIs until a separate, later change flips this note and starts requiring cityrp.network for the call sites described in tier 2.
Move a call site as a deliberate change, with its legacy registration removed from networking/sh_init.lua in the same commit — not opportunistically off this note alone. See networking.md and issue #3207 for the full migration plan.