terminalCode Example

Sandbox: Ownable component — prop protection with IPhysgunEvent and IToolgunEvent

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

Ownable Component — Prop Protection and Physgun/Toolgun Access Control

The Ownable component tracks which Connection spawned a GameObject. It implements IPhysgunEvent and IToolgunEvent to enforce ownership checks when sb.ownership_checks is enabled.

CSHARP
public sealed class Ownable : Component, IPhysgunEvent, IToolgunEvent
{
    [Sync( SyncFlags.FromHost )]
    private Guid _ownerId { get; set; }

    [Property, ReadOnly, JsonIgnore]
    public Connection Owner
    {
        get => Connection.All.FirstOrDefault( c => c.Id == _ownerId );
        set => _ownerId = value?.Id ?? Guid.Empty;
    }

    // Convenience: add Ownable to a GameObject and set its owner in one call
    public static Ownable Set( GameObject go, Connection owner )
    {
        var ownable = go.GetOrAddComponent<Ownable>();
        ownable.Owner = owner;
        return ownable;
    }

    // ConVar: sb.ownership_checks — replicated server setting
    [ConVar( "sb.ownership_checks", ConVarFlags.Replicated | ConVarFlags.Server | ConVarFlags.GameSetting )]
    public static bool OwnershipChecks { get; set; } = false;

    internal bool CallerHasAccess( Connection caller ) => HasAccess( caller, Owner );

    public static bool HasAccess( Connection caller, Connection owner )
    {
        if ( !OwnershipChecks ) return true;
        if ( caller.IsHost ) return true; // Host always has access
        return caller == owner;
    }

    void IPhysgunEvent.OnPhysgunGrab( IPhysgunEvent.GrabEvent e )
    {
        if ( !CallerHasAccess( e.Grabber ) )
            e.Cancelled = true;
    }

    void IToolgunEvent.OnToolgunSelect( IToolgunEvent.SelectEvent e )
    {
        if ( !CallerHasAccess( e.User ) )
            e.Cancelled = true;
    }
}

// Extension method for easy access checks
public static class OwnableExtensions
{
    public static bool HasAccess( this GameObject go, Connection caller )
    {
        if ( go.Components.TryGet<Ownable>( out var ownable ) )
            return ownable.CallerHasAccess( caller );
        return true; // No Ownable = no restriction
    }
}

IPhysgunEvent and IToolgunEvent interfaces

CSHARP
public interface IPhysgunEvent : ISceneEvent<IPhysgunEvent>
{
    public class GrabEvent
    {
        public Connection Grabber { get; init; }
        public bool Cancelled { get; set; }
    }

    void OnPhysgunGrab( GrabEvent e ) { }
}

public interface IToolgunEvent : ISceneEvent<IToolgunEvent>
{
    public class SelectEvent
    {
        public Connection User { get; init; }
        public bool Cancelled { get; set; }
    }

    void OnToolgunSelect( SelectEvent e ) { }
}

Usage when spawning props

CSHARP
// PropSpawner sets ownership immediately after creating the prop
var go = new GameObject( false, "prop" );
go.AddComponent<Prop>().Model = model;
Ownable.Set( go, player.Network.Owner );
go.NetworkSpawn( true, null );

Key points

Was this helpful?