UI/SpawnMenu/Spawnlists/SpawnlistCollection.cs
namespace Sandbox;
/// <summary>
/// Class that handles all the spawnlist data fetching and workshop fetching so it's not a huge mess everywhere.
/// </summary>
public class SpawnlistCollection
{
public record Entry(
string Icon,
string Name,
Storage.Entry StorageEntry,
ulong WorkshopId,
bool IsEditable
);
/// <summary>
/// Raised whenever the visible entry list changes.
/// </summary>
public event Action Changed;
/// <summary>
/// Raised after a workshop install, with the installed entry name.
/// </summary>
public event Action<string> Installed;
/// <summary>
/// Raised after an uninstall.
/// </summary>
public event Action Uninstalled;
public IReadOnlyList<Entry> Entries => _entries;
/// <summary>
/// Number of cookie-tracked workshop entries not yet resolved from storage or the cloud.
/// Non-zero while the initial cloud fetch is in progress.
/// </summary>
public int PendingCount
{
get
{
if ( !_loading ) return 0;
var loaded = _entries.Select( e => e.WorkshopId ).Where( id => id > 0 ).ToHashSet();
return new SavedSpawnlists().Installed.Count( id => !loaded.Contains( id ) );
}
}
List<Entry> _entries = new();
Dictionary<ulong, Storage.Entry> _cloudEntries = new();
bool _queried;
bool _loading;
/// <summary>
/// Rebuilds the visible list and triggers a cloud re-fetch.
/// </summary>
public void Refresh()
{
_queried = false;
Rebuild();
}
/// <summary>
/// Install a workshop item, persist its ID to cookie, and refresh.
/// </summary>
public async Task InstallAsync( Storage.QueryItem item )
{
var entry = await item.Install();
if ( entry is null ) return;
var saved = new SavedSpawnlists();
saved.Add( item.Id );
saved.Save();
Installed?.Invoke( item.Title );
Refresh();
}
/// <summary>
/// Uninstall a workshop-installed entry: remove from cookie, cloud list, and rebuild.
/// </summary>
public void Uninstall( ulong workshopId )
{
if ( workshopId == 0 ) return;
var saved = new SavedSpawnlists();
if ( saved.Remove( workshopId ) )
saved.Save();
_cloudEntries.Remove( workshopId );
Uninstalled?.Invoke();
Rebuild();
}
/// <summary>
/// Delete a locally editable spawnlist and rebuild.
/// </summary>
public void Delete( Storage.Entry entry )
{
try
{
SpawnlistData.Delete( entry );
}
catch ( Exception e )
{
Log.Warning( e, $"Something went wrong while deleting an entry: {e.Message}" );
}
finally
{
Refresh();
}
}
struct SavedSpawnlists
{
public List<ulong> Installed { get; set; }
public SavedSpawnlists()
{
Installed = Game.Cookies.Get<List<ulong>>( "spawnlists.installed", new() );
}
public void Save() => Game.Cookies.Set( "spawnlists.installed", Installed );
public void Add( ulong id ) { if ( !Installed.Contains( id ) ) Installed.Add( id ); }
public bool Remove( ulong id ) => Installed.Remove( id );
public HashSet<ulong> ToHashSet() => Installed.ToHashSet();
}
void Rebuild()
{
var installedIds = new SavedSpawnlists().ToHashSet();
var result = new List<Entry>();
foreach ( var storageEntry in SpawnlistData.GetAll() )
{
SpawnlistData data;
try
{
if ( storageEntry.Files.IsReadOnly ) continue;
data = SpawnlistData.Load( storageEntry );
}
catch
{
continue;
}
result.Add( new Entry( "📁", data.Name, storageEntry, 0, true ) );
}
foreach ( var (workshopId, storageEntry) in _cloudEntries )
{
if ( !installedIds.Contains( workshopId ) ) continue;
var data = SpawnlistData.Load( storageEntry );
result.Add( new Entry( "☁️", data.Name, storageEntry, workshopId, false ) );
}
_entries = result;
if ( !_queried ) _loading = true;
Changed?.Invoke();
if ( !_queried )
{
_queried = true;
_ = FetchCloudSpawnlists();
}
}
async Task FetchCloudSpawnlists()
{
var query = new Storage.Query();
query.KeyValues["package"] = "facepunch.sandbox";
query.KeyValues["type"] = "spawnlist";
query.Author = Game.SteamId;
var result = await query.Run();
if ( result?.Items is not null )
{
foreach ( var item in result.Items )
{
if ( _cloudEntries.ContainsKey( item.Id ) ) continue;
var installed = await item.Install();
if ( installed == null ) continue;
_cloudEntries[item.Id] = installed;
}
}
var missingIds = new SavedSpawnlists().Installed
.Where( id => !_cloudEntries.ContainsKey( id ) )
.ToList();
if ( missingIds.Count > 0 )
{
var missingResult = await new Storage.Query { FileIds = missingIds }.Run();
if ( missingResult?.Items is not null )
{
foreach ( var item in missingResult.Items )
{
if ( _cloudEntries.ContainsKey( item.Id ) ) continue;
var installed = await item.Install();
if ( installed == null ) continue;
_cloudEntries[item.Id] = installed;
}
}
}
_loading = false;
Rebuild();
}
}