UI/SpawnMenu/Spawnlists/SpawnlistResource.cs

Resource class for spawnlist assets used by the spawn menu. Defines metadata (title, description, icon, parent, order), a list of entries, conversion to runtime SpawnlistData, visibility rules based on mounted content, and an editor asset icon.

File Access
namespace Sandbox;

[AssetType( Name = "Sandbox Spawnlist", Extension = "spawn", Category = "Sandbox", Flags = AssetTypeFlags.NoEmbedding )]
public class SpawnlistResource : GameResource
{
	[Property]
	public string Title { get; set; }

	[Property]
	public string Description { get; set; }

	/// <summary>
	/// Material icon shown next to the list in the spawn menu sidebar.
	/// </summary>
	[Property, IconName]
	public string Icon { get; set; } = "collections_bookmark";

	/// <summary>
	/// Mount required to see this list in the spawn menu. Leave empty for no requirement.
	/// </summary>
	public string RequiredMount { get; set; }

	/// <summary>
	/// Nests this list under another one in the spawn menu tree. Leave empty for a top level list.
	/// </summary>
	[Property]
	public SpawnlistResource Parent { get; set; }

	/// <summary>
	/// Lists are sorted by this, then by title.
	/// </summary>
	[Property]
	public int Order { get; set; }

	[Property]
	public List<Entry> Items { get; set; } = new();

	public class Entry
	{
		[Property]
		public SpawnlistItemKind Kind { get; set; }

		/// <summary>
		/// A raw spawn ident, for anything that isn't a local model or entity,
		/// e.g. <c>prop:facepunch.wooden_crate</c>. Used when no model or entity is set.
		/// </summary>
		[Property, ShowIf( nameof( Kind ), SpawnlistItemKind.Item )]
		public string Ident { get; set; }

		/// <summary>
		/// Item label, or the section heading for a title row.
		/// </summary>
		[Property, HideIf( nameof( Kind ), SpawnlistItemKind.Divider )]
		public string Title { get; set; }

		/// <summary>
		/// Optional thumbnail override. Defaults to the content's own thumbnail.
		/// </summary>
		[Property, ShowIf( nameof( Kind ), SpawnlistItemKind.Item ), IconName]
		public string Icon { get; set; }

		public string GetIdent()
		{
			var ident = Ident.Trim();
			if ( !ident.EndsWith( ".vmdl" ) ) ident += ".vmdl";
			return ident;
		}

		public override string ToString()
		{
			return $"({Kind}) {GetIdent() ?? Title}";
		}
	}

	/// <summary>
	/// The title in the player's language. Titles can be <c>#phrase</c> references.
	/// </summary>
	[System.Text.Json.Serialization.JsonIgnore]
	public string DisplayName => string.IsNullOrWhiteSpace( Title ) ? ResourceName : LocalizedText.Resolve( Title );

	/// <summary>
	/// The resource in the shape the spawnlist view draws. Items get stable ids
	/// from their position so the view can tell when the resource changes.
	/// </summary>
	public SpawnlistData ToData()
	{
		var data = new SpawnlistData { Name = DisplayName, Description = Description ?? "" };

		for ( var i = 0; i < (Items?.Count ?? 0); i++ )
		{
			var entry = Items[i];
			if ( entry is null ) continue;

			var item = new SpawnlistItem
			{
				Id = new Guid( i + 1, 0, 0, new byte[8] ),
				Kind = entry.Kind,
				Title = entry.Title,
			};

			if ( entry.Kind == SpawnlistItemKind.Item )
			{
				item.Ident = entry.GetIdent();
				if ( item.Ident is null ) continue;
				item.Icon = string.IsNullOrWhiteSpace( entry.Icon ) ? null : entry.Icon;
			}

			data.Items.Add( item );
		}

		return data;
	}

	bool ShouldShow()
	{
		if ( !string.IsNullOrWhiteSpace( RequiredMount ) )
		{
			var mount = Sandbox.Mounting.Directory.Get( RequiredMount );
			if ( mount is null || !mount.IsMounted ) return false;
		}

		return true;
	}

	/// <summary>
	/// Every authored spawnlist the player should see, in display order.
	/// </summary>
	public static IEnumerable<SpawnlistResource> GetVisible()
	{
		return ResourceLibrary.GetAll<SpawnlistResource>()
			.Where( x => x.ShouldShow() )
			.OrderBy( x => x.Order )
			.ThenBy( x => x.DisplayName, StringComparer.OrdinalIgnoreCase );
	}

	protected override Bitmap CreateAssetTypeIcon( int width, int height )
	{
		return CreateSimpleAssetTypeIcon( "📋", width, height, "#3fa9f5" );
	}
}