terminalCode Example
Sandbox: LimitsSystem — server-side per-player spawn and tool limits via ConVars
LimitsSystem — Server-Side Spawn and Tool Limits
LimitsSystem is a GameObjectSystem that enforces configurable per-player limits on props, constraints, and tool entities. It listens to ISpawnEvents and IToolActionEvents.ConVars (all replicated server settings)
CSHARP
public sealed class LimitsSystem : GameObjectSystem<LimitsSystem>, Global.ISpawnEvents, IToolActionEvents
{
[ConVar( "sb.limit.props", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxPropsPerPlayer { get; set; } = -1; // -1 = unlimited
[ConVar( "sb.limit.explosives", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxExplosivesPerPlayer { get; set; } = -1;
[ConVar( "sb.limit.balloons", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxBalloons { get; set; } = -1;
[ConVar( "sb.limit.constraints", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxConstraints { get; set; } = -1;
[ConVar( "sb.limit.thrusters", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxThrusters { get; set; } = -1;
[ConVar( "sb.limit.hoverballs", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxHoverballs { get; set; } = -1;
[ConVar( "sb.limit.wheels", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxWheels { get; set; } = -1;
[ConVar( "sb.limit.emitters", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
public static int MaxEmitters { get; set; } = -1;
}How limits are enforced
CSHARP
void Global.ISpawnEvents.OnSpawn( Global.ISpawnEvents.SpawnData e )
{
if ( e.Player is null ) return;
// Duplicator: batch pre-check — reject entire dupe if it would exceed limits
if ( e.Spawner is DuplicatorSpawner dupeSpawner )
{
var dupeObjectCount = dupeSpawner.Dupe?.Objects?.Count ?? 0;
if ( MaxPropsPerPlayer >= 0 && dupeObjectCount > 0 )
{
var current = Count( e.Player.SteamId, go => go.GetComponent<Prop>().IsValid() );
if ( current + dupeObjectCount > MaxPropsPerPlayer )
{
e.Cancelled = true;
NotifyLimit( e.Player, "props", MaxPropsPerPlayer );
return;
}
}
return;
}
if ( MaxPropsPerPlayer >= 0 && e.Spawner is PropSpawner )
{
var count = Count( e.Player.SteamId, go => go.GetComponent<Prop>().IsValid() );
if ( count >= MaxPropsPerPlayer )
{
e.Cancelled = true;
NotifyLimit( e.Player, "props", MaxPropsPerPlayer );
}
}
}
void IToolActionEvents.OnToolAction( IToolActionEvents.ActionData e )
{
if ( e.Input == ToolInput.Reload ) return; // Reload = remove, never limit
if ( e.Player is null ) return;
// Generic helper: check if tool type matches and count existing entities
if ( CheckToolLimit<ThrusterTool, ThrusterEntity>( e, MaxThrusters ) ) return;
if ( CheckToolLimit<HoverballTool, HoverballEntity>( e, MaxHoverballs, ToolInput.Primary ) ) return;
if ( CheckToolLimit<WheelTool, WheelEntity>( e, MaxWheels, ToolInput.Primary ) ) return;
// Constraints use tag-based counting
if ( MaxConstraints >= 0 && ( e.Tool is BaseConstraintToolMode || e.Tool is KeepUpright ) )
{
var count = Count( e.Player.SteamId, go => go.Tags.Contains( "constraint" ) );
if ( count >= MaxConstraints )
{
e.Cancelled = true;
NotifyLimit( e.Player, GetToolName( e.Tool ), MaxConstraints );
}
}
}
// Objects are tracked in OnPostSpawn and OnPostToolAction
void IToolActionEvents.OnPostToolAction( IToolActionEvents.PostActionData e )
{
if ( e.Player.IsValid() && e.CreatedObjects is { Count: > 0 } )
Track( e.Player.SteamId, e.CreatedObjects );
}Key points
- All limit ConVars default to -1 (unlimited) — set to 0 to block entirely
- Per-player object lists are maintained in a Dictionary<long, List<GameObject>> keyed by SteamId
- Duplicator spawns are pre-checked as a batch before any objects are created
- Constraints are identified by the "constraint" tag on their GameObjects
- NotifyLimit sends a notice to the player's screen via the Notices system
Was this helpful?