terminalCode Example

Sandbox: LimitsSystem — server-side per-player spawn and tool limits via ConVars

calendar_today May 4, 2026 schedule ~1 min read person patrickjr verified 50

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?