Player limits
Last updated
May be stale
This page was last updated ago, on 5 October 2026. Check what it says against the code before relying on it.
Every limit staff or team can hand out — props, signs, notes, properties, ragdolls, lights and
boomboxes — is registered as a named type in cityrp.limits
(gamemode/core/libraries/sh_playerlimits.lua, with the server half in sv_playerlimits.lua), so
!limit stays one command and the code enforcing a limit reads back exactly the value that was
set.
Before this, !limit carried its own hardcoded sign/prop/note table, each enforcement site
read its own storage directly, and adding a fourth type meant editing both ends. !proplimit was
also deprecated in favour of a !limit that Event Managers could not reach.
How a limit resolves
Each type is a cityrp.layers
stack whose apply publishes the resolved number to a
cityrp.network var:
default layer (rank / REP / configuration) ─┐
admin layer (!limit) ─┤ combine ─→ cityrp_limit_<type> ─→ both realms
...anything else a system pushes later ─┘
combine takes the admin layer if one is set, otherwise default, otherwise the type's
registration fallback. That is the whole point of the layering: !limit and the rank/REP default
are separate contributions, so refreshing one never discards the other — a rank change recomputes
default while an admin extension keeps sitting above it, and a reset drops back onto whatever
default currently holds without having to recompute it at the call site.
Props already worked this way before this library existed and keep their own richer stack
(core/metatables/sv_player.lua): rank, member, hours, rep and giveprops on top of the
same admin layer. That is the shape any other type can grow into — a membership sign bonus, an
event ragdoll allowance — without touching !limit or the enforcement site.
The network vars are :SetLocalPlayer() scoped: nothing reads another player's limits, so
broadcasting every player's seven limits to every client would be pure cost. -1 is unlimited,
so they are signed (:Int()).
Keeping defaults fresh
cityrp.limits:RefreshDefaults(ply) recomputes every type's default layer. Call it wherever an
input to a default changes; it is already wired into the points that existed for prop limits — data
load, rep_load, SetRank, membership resolution, and the playtime tick. sbox_maxlights and
sbox_maxlamps additionally repush the light default through a cvars.AddChangeCallback, since
nothing else fires when a ConVar moves.
Player:RefreshPropLimitDefault() still exists and still does props only — prefer
cityrp.limits:RefreshDefaults.
The command
!limit <player> -- list every limit you can adjust on that player
!limit <player> <type> -- read one of them
!limit <player> <type> <n|reset|inf> -- set it
reset (or 0, default) clears the override and puts the player back on the default for their
rank/REP. inf (or -1, unlimited) removes the limit entirely. Values are resolved numerically,
so -1.00 and 0.0 mean the same as -1 and 0. Types accept plurals and a few
obvious synonyms (props, lamps, radios, houses, …).
!proplimit <player> <amount> still works, deprecated, and forwards to the prop type.
Who can set what
| Access | Flag | Types |
|---|---|---|
| Moderator S2 and above | @ |
every registered type |
| Assistants, Event Managers | - |
only the types marked team — currently prop |
The command is registered on - so Assistants and Event Managers can reach it at all; the type
they asked for is then re-checked against cityrp.limits:CanSet, which is what actually holds them
to prop limits. An Assistant who asks for a type they cannot set is told which types they can.
Registered types
| Type | Default | Layer stack | Enforced in |
|---|---|---|---|
prop |
rank + member + hours + REP, then giveprops |
proplimit |
GM:PlayerSpawnProp |
sign |
min(REP, 6) |
limit:sign |
items/other/sh_sign.lua |
note |
Maximum Notes |
limit:note |
/note |
property |
Maximum Properties |
limit:property |
doors plugin, on purchase |
ragdoll |
Maximum Ragdolls, unlimited for Assistants |
limit:ragdoll |
GM:PlayerSpawnRagdoll |
boombox |
Maximum Boomboxes |
limit:boombox |
base_spawnable's limiter |
light |
sbox_maxlights |
limit:light |
PlayerCheckLimit, for lights and lamps |
Each publishes to cityrp_limit_<type>.
light covers both sandbox counters, because the light and lamp tools are unlocked together
(cityrp.BlockedTools, GM:CanTool). Each counter keeps its own sbox_max* as a hard ceiling, so
sharing one limit type can never lift lamps to the (often higher) lights cap.
Adding a type
A Register call plus swapping the enforcement site over to :Get/:CanSpawn:
cityrp.limits:Register("piano", {
aliases = {"pianos"},
fallback = cityrp.configuration["Maximum Pianos"],
max = 16,
default = function() return cityrp.configuration["Maximum Pianos"] end
})
max is the type's ceiling: combine clamps to it, !limit refuses anything above it, and the
network var's codec is sized to it. It defaults to cityrp.limits.DEFAULT_MAX (64); props raise it
to 1024, being the one type that legitimately runs into the hundreds.
Register derives the network var (cityrp_limit_piano) and the layer name (limit:piano) from
the id and registers the var itself, and the stack is defined for every type that doesn't bring
one of its own: sv_playerlimits.lua defines one for each type registered before it loads, and
Register defines one through :DefineLayer for each type registered after, so a plugin's type
works the same as a core one. Nothing else needs wiring —
!limit picks the type up from the registry.
then, where the limit is enforced:
local allowed, limit = cityrp.limits:CanSpawn(ply, "piano")
if not allowed then
ply:Notify("You can only spawn " .. limit .. "!", NOTIFY_ERROR)
return false
end
CanSpawn returns the limit already formatted for the player ("3 pianos", "unlimited"), so the
call site never has to pluralise it or special-case -1.
A type whose base is more than one contribution (as prop's is) passes refresh and set of its
own instead of default, and defines its stack wherever those other layers are pushed from.
Registration happens at file scope, before cityrp.network's registry seals on Initialize. That
is also why the file is named sh_playerlimits rather than sh_limits: includeCore("libraries")
loads alphabetically, and it has to come after sh_network.lua.
Values
cityrp.limits.UNLIMITED is -1. cityrp.limits:Set(ply, type, nil) clears the admin layer and
drops the player back onto their default; Reset is the same call spelled out. There is no
"no override" sentinel in the stored value — an absent layer is absent, which is exactly what a
layer stack represents and what an NSVar could not.
API
These are methods, called with : — the library recurses into itself (self:Resolve, self.logger,
self.UNLIMITED) rather than back through the namespace.
Shared (sh_playerlimits.lua): :Register, :Resolve, :GetAll, :Get, :IsUnlimited,
:Describe, :CanSpawn, :CanSet, :GetSettable. Server (sv_playerlimits.lua): :Set,
:Reset, :RefreshDefaults, :DefineLayer. Constants (UNLIMITED, DEFAULT_MAX, TEAM_ACCESS, …) and the
types/aliases tables stay plain fields.
cityrp.limits.PlayerCheckLimit stays a plain function: it is handed straight to hook.Add, which
would pass the hook's own arguments where self is expected.
CanSet and GetSettable go through Player:HasAccess, which is only accurate on the server (and
clientside for LocalPlayer), so keep those two on the server.