UI/SpawnMenu/Spawnlists/SpawnlistData.cs
namespace Sandbox;
using Sandbox.UI;
using System.Text.Json.Serialization;
public enum SpawnlistItemKind
{
Item,
Divider,
Title
}
/// <summary>
/// A spawnlist item -- lots of cleanup needed, docs, etc
/// </summary>
public class SpawnlistItem : IMixedVirtualGridFullRow
{
[JsonPropertyName( "id" )]
public Guid Id { get; set; }
[JsonPropertyName( "kind" )]
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
public SpawnlistItemKind Kind { get; set; }
[JsonPropertyName( "ident" )]
public string Ident { get; set; }
[JsonPropertyName( "title" )]
public string Title { get; set; }
[JsonPropertyName( "icon" )]
public string Icon { get; set; }
[JsonIgnore]
public bool IsContent => Kind == SpawnlistItemKind.Item;
[JsonIgnore]
public bool IsFullRow => !IsContent;
[JsonIgnore]
public float Height => Kind == SpawnlistItemKind.Title ? 56f : 32f;
public static string MakeIdent( string type, string path, string source = "local" )
{
// TODO: hate this special case
if ( type == "dupe" )
return $"dupe.{source}:{path}";
return $"{type}:{path}";
}
public static (string Type, string Path, string Source) ParseIdent( string ident )
{
if ( string.IsNullOrEmpty( ident ) )
return (null, null, "local");
var colonIndex = ident.IndexOf( ':' );
if ( colonIndex < 0 )
return (ident, ident, "local");
var prefix = ident[..colonIndex];
var data = ident[(colonIndex + 1)..];
// TODO: hate this special case
if ( prefix.StartsWith( "dupe." ) )
{
var source = prefix["dupe.".Length..];
return ("dupe", data, source);
}
return (prefix, data, "local");
}
}
public class SpawnlistData
{
/// <summary>
/// Raised whenever a new spawnlist is created, so UI can refresh without needing a panel ancestor walk.
/// </summary>
public static event Action SpawnlistCreated;
[JsonPropertyName( "name" )]
public string Name { get; set; } = "#spawnmenu.spawnlist.untitled";
[JsonPropertyName( "description" )]
public string Description { get; set; } = "";
[JsonPropertyName( "items" )]
public List<SpawnlistItem> Items { get; set; } = new();
public static SpawnlistData Create( string name )
{
return Create( name, out _ );
}
public static SpawnlistData Create( string name, out Storage.Entry entry )
{
var data = new SpawnlistData { Name = name };
entry = Storage.CreateEntry( "spawnlist" );
entry.SetMeta( "name", name );
Save( entry, data );
SpawnlistCreated?.Invoke();
return data;
}
public static void Save( Storage.Entry entry, SpawnlistData data )
{
Normalize( data, true );
var contentItems = data.Items.Where( item => item.IsContent ).ToList();
entry.Files.WriteJson( "/spawnlist.json", data );
entry.SetMeta( "name", data.Name );
entry.SetMeta( "item_count", contentItems.Count );
entry.SetMeta( "metadata_version", 2 );
entry.SetMeta( "preview_items", Json.Serialize( contentItems.Take( 6 ).ToList() ) );
var contentTypes = contentItems
.Select( item => SpawnlistItem.ParseIdent( item.Ident ).Type ?? "other" )
.GroupBy( type => type, StringComparer.OrdinalIgnoreCase )
.ToDictionary( group => group.Key.ToLowerInvariant(), group => group.Count() );
entry.SetMeta( "content_types", Json.Serialize( contentTypes ) );
}
public static SpawnlistData Load( Storage.Entry entry )
{
if ( !entry.Files.FileExists( "/spawnlist.json" ) )
return new SpawnlistData { Name = entry.GetMeta<string>( "name" ) ?? "Untitled" };
var data = entry.Files.ReadJson<SpawnlistData>( "/spawnlist.json" )
?? new SpawnlistData { Name = "Untitled" };
Normalize( data, !entry.Files.IsReadOnly );
return data;
}
private static bool Normalize( SpawnlistData data, bool assignIds )
{
var changed = data.Items is null;
data.Items ??= new();
if ( !assignIds ) return changed;
foreach ( var item in data.Items )
{
if ( item.Id != Guid.Empty ) continue;
item.Id = Guid.NewGuid();
changed = true;
}
return changed;
}
public static IEnumerable<Storage.Entry> GetAll()
{
return Storage.GetAll( "spawnlist" ).OrderByDescending( x => x.Created );
}
public static void Rename( Storage.Entry entry, string newName )
{
var data = Load( entry );
data.Name = newName;
Save( entry, data );
}
public static void Delete( Storage.Entry entry )
{
entry.Delete();
}
public static void Publish( Storage.Entry entry )
{
// Refresh the public metadata manifest before every publish/sync so older
// local spawnlists gain content previews without needing to be edited first.
Save( entry, Load( entry ) );
var options = new Modals.WorkshopPublishOptions { Title = entry.GetMeta<string>( "name", "Untitled" ), Description = entry.GetMeta<string>( "description", "" ) };
entry.Publish( options );
}
public static void AddItem( Storage.Entry entry, SpawnlistItem item )
{
var data = Load( entry );
if ( item.Id == Guid.Empty ) item.Id = Guid.NewGuid();
data.Items.Add( item );
Save( entry, data );
}
public static Guid AddMarker( Storage.Entry entry, SpawnlistItemKind kind, string title = null )
{
if ( kind == SpawnlistItemKind.Item ) throw new ArgumentOutOfRangeException( nameof( kind ) );
var data = Load( entry );
var item = new SpawnlistItem { Id = Guid.NewGuid(), Kind = kind, Title = title };
data.Items.Add( item );
Save( entry, data );
return item.Id;
}
public static void MoveItem( Storage.Entry entry, Guid id, int destinationIndex )
{
var data = Load( entry );
var sourceIndex = data.Items.FindIndex( item => item.Id == id );
if ( sourceIndex < 0 ) return;
var item = data.Items[sourceIndex];
data.Items.RemoveAt( sourceIndex );
if ( sourceIndex < destinationIndex ) destinationIndex--;
destinationIndex = destinationIndex.Clamp( 0, data.Items.Count );
data.Items.Insert( destinationIndex, item );
Save( entry, data );
}
public static void RenameItem( Storage.Entry entry, Guid id, string title )
{
var data = Load( entry );
var item = data.Items.FirstOrDefault( item => item.Id == id );
if ( item is null ) return;
item.Title = title?.Trim() ?? "";
Save( entry, data );
}
public static void RemoveItem( Storage.Entry entry, Guid id )
{
var data = Load( entry );
if ( data.Items.RemoveAll( item => item.Id == id ) > 0 )
Save( entry, data );
}
public static void RemoveItem( Storage.Entry entry, int index )
{
var data = Load( entry );
if ( index >= 0 && index < data.Items.Count )
{
data.Items.RemoveAt( index );
Save( entry, data );
}
}
public static void PopulateContextMenu( Sandbox.UI.Menu menu, Panel sourcePanel, SpawnlistItem item, Storage.Entry skipEntry = null )
{
var entries = GetAll()
.Where( e => skipEntry is null || e.Id != skipEntry.Id )
.ToList();
if ( entries.Count > 0 )
{
var sub = menu.AddMenu( "#spawnmenu.spawnlist.add_to_submenu", "📋" );
foreach ( var entry in entries )
{
var data = Load( entry );
var capturedEntry = entry;
sub.AddOption( data.Name, "📋", () => AddItem( capturedEntry, item ) );
}
menu.AddSeparator();
}
menu.AddOption( "#spawnmenu.spawnlist.create_new_option", "➕", () =>
{
var popup = new SpawnlistCreatePopup
{
Name = item.Title ?? "New Spawnlist",
InitialItem = item,
Parent = sourcePanel.FindPopupPanel()
};
var spawnMenu = sourcePanel.Ancestors.OfType<SpawnMenu>().FirstOrDefault();
if ( spawnMenu is not null )
popup.OnEntryCreated = spawnMenu.OpenSpawnlist;
} );
var (type, path, source) = SpawnlistItem.ParseIdent( item.Ident );
var spawner = ISpawner.Create( type, path, source );
var fullIdent = spawner?.FullIdent;
if ( !string.IsNullOrEmpty( fullIdent ) )
{
menu.AddSeparator();
menu.AddOption( "Open in Workshop", "🌐", () =>
{
Game.Overlay.ShowPackageModal( fullIdent );
} );
}
}
}