CityRP Documentation Repository

Player limits

Last updated

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.