terminalCode Example
Sandbox: Ownable component — prop protection with IPhysgunEvent and IToolgunEvent
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
- _ownerId is synced from host — clients can read ownership but not set it
- Host is always exempt from ownership checks regardless of sb.ownership_checks
- Objects without Ownable are freely interactable by everyone
- IPhysgunEvent / IToolgunEvent are ISceneEvent<T> — implement on any component to intercept
Was this helpful?