Code/SboxSuperuser/Systems/PropTrackingSystem.cs

A host-side GameObjectSystem for a prop protection mod. Every half second it walks the scene roots, records the network owner's SteamId for each non-player object into a prop ownership registry, prunes registry entries whose objects are gone, and removes a disconnected player's props once their grace period expires unless they rejoined.

using Sandbox;

namespace SboxSuperuser.Systems;

/// <summary>
/// On the host, records who spawned each object so prop protection still knows after they leave, and removes a
/// player's props a while after they disconnect unless they come back. Player bodies are never counted as props.
/// </summary>
internal sealed class PropTrackingSystem : GameObjectSystem<PropTrackingSystem>
{
	private const float Interval = 0.5f;

	private readonly Dictionary<string, DateTimeOffset> _left = new( StringComparer.Ordinal );
	private RealTimeSince _sinceSweep;

	public PropTrackingSystem( Scene scene ) : base( scene ) => Listen( Stage.FinishUpdate, 49, Sweep, nameof( PropTrackingSystem ) );

	/// <summary>Starts the countdown to removing this player's props; a null delay keeps them.</summary>
	internal void PlayerLeft( string steamId, TimeSpan? delay )
	{
		if ( delay is { } wait ) _left[ steamId ] = DateTimeOffset.UtcNow + wait;
	}

	internal void PlayerReturned( string steamId ) => _left.Remove( steamId );

	private void Sweep()
	{
		if ( _sinceSweep < Interval || !Networking.IsHost || Superuser.Props is not { } props ) return;
		_sinceSweep = 0;
		foreach ( var root in Scene.Children )
		{
			if ( root.Network.Owner is not { } owner || root.GetComponentInChildren<PlayerController>( true ) is not null ) continue;
			props.Record( root.Id, owner.SteamId.ToString() );
		}
		foreach ( var id in props.All )
			if ( Scene.Directory.FindByGuid( id ) is null ) props.Forget( id );
		if ( Superuser.PropWorld is { } world ) CleanUpAfterLeavers( props, world );
	}

	private void CleanUpAfterLeavers( Core.Props.PropOwnership props, Engine.Props.IPropWorld world )
	{
		if ( _left.Count == 0 ) return;
		var now = DateTimeOffset.UtcNow;
		foreach ( var (steamId, due) in _left.ToArray() )
		{
			if ( due > now ) continue;
			_left.Remove( steamId );
			var objects = props.ObjectsOf( steamId );
			var removed = world.Remove( objects );
			foreach ( var id in objects ) props.Forget( id );
			if ( removed > 0 ) Log.Info( $"[Superuser] Removed {removed} props left behind by {steamId}." );
		}
	}
}