Spawner/PropSpawner.cs
/// <summary>
/// Payload for spawning a prop model from a cloud ident.
/// </summary>
public sealed class PropSpawner : ISpawner
{
	public string DisplayName { get; private set; }
	public string FullIdent => Path is not null && !Path.EndsWith( ".vmdl" ) && !Path.EndsWith( ".vmdl_c" ) ? Path : null;
	public string Icon => Path;
	public string Data => Path;
	public BBox Bounds => Model?.Bounds ?? default;
	public bool IsReady => Model is not null && !Model.IsError;
	public Task<bool> Loading { get; }

	public Model Model { get; private set; }
	public string Path { get; }

	public PropSpawner( string path )
	{
		Path = path;
		DisplayName = null;
		Loading = LoadAsync();
	}

	private async Task<bool> LoadAsync()
	{
		// Try local/installed first, then fall back to cloud
		if ( Path.EndsWith( ".vmdl" ) || Path.EndsWith( ".vmdl_c" ) )
		{
			Model = await ResourceLibrary.LoadAsync<Model>( Path );
			if ( Model is not null )
			{
				DisplayName = Model.ResourceName;
				return true;
			}
		}

		Model = await Cloud.Load<Model>( Path );

		if ( Model is not null )
		{
			DisplayName = Model.ResourceName ?? DisplayName;
		}

		return IsReady;
	}

	public void DrawPreview( Transform transform, Material overrideMaterial )
	{
		if ( !IsReady ) return;

		Game.ActiveScene.DebugOverlay.Model( Model, transform: transform, overlay: false, materialOveride: overrideMaterial );
	}

	public Task<List<GameObject>> Spawn( Transform transform, Player player )
	{
		var depth = -Bounds.Mins.z;
		transform.Position += transform.Up * depth;

		var go = new GameObject( false, "prop" );
		go.Tags.Add( "removable" );
		go.WorldTransform = transform;

		var prop = go.AddComponent<Prop>();
		prop.Model = Model;

		if ( player.IsValid() )
			Ownable.Set( go, player.Network.Owner );

		if ( (Model.Physics?.Parts?.Count ?? 0) == 0 )
		{
			var collider = go.AddComponent<BoxCollider>();
			collider.Scale = Model.Bounds.Size;
			collider.Center = Model.Bounds.Center;
			go.AddComponent<Rigidbody>();
		}

		go.NetworkSpawn( true, null );

		return Task.FromResult( new List<GameObject> { go } );
	}
}