Service that preloads sound resources on demand for a soundboard. It tracks per-sound load state, queues prioritized loads, limits concurrent async loads, supports enqueueing a play when ready, and signals state changes via events.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SSoundboard;
/// <summary>
/// On-demand sound loading. Prefer TryGet (already compiled); LoadAsync only on click.
/// </summary>
public static class SoundPreloadService
{
enum LoadState { Pending, Loading, Ready, Failed }
static readonly Dictionary<string, LoadState> _states = new( StringComparer.OrdinalIgnoreCase );
static readonly List<string> _priority = new();
static readonly Dictionary<string, SoundPad> _pendingPlays = new( StringComparer.OrdinalIgnoreCase );
static int _loaded;
static int _registered;
static int _activeLoads;
static bool _initialized;
static bool _startupComplete;
const int MaxConcurrentLoads = 2;
const int StartupWarmCap = 32;
public static event Action Changed;
public static int LoadedCount => _loaded;
public static bool IsInitialized => _initialized;
public static bool IsStartupComplete => _startupComplete;
/// <summary>Bumps when pending-play set changes — cheap UI rebuild signal.</summary>
public static int PendingRevision { get; private set; }
public static void Initialize()
{
if ( _initialized )
return;
_initialized = true;
var warmed = 0;
foreach ( var soundId in SoundBoardData.StartupSoundIds )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
continue;
if ( TryMarkReadyIfPresent( Normalize( soundId ) ) )
warmed++;
if ( warmed >= StartupWarmCap )
break;
}
_startupComplete = true;
Changed?.Invoke();
}
static void EnsureInitialized()
{
if ( !_initialized )
Initialize();
}
public static bool IsReady( string soundId )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return false;
EnsureInitialized();
var key = Normalize( soundId );
if ( _states.TryGetValue( key, out var state ) )
{
if ( state == LoadState.Ready )
return true;
if ( state is LoadState.Failed or LoadState.Loading )
return false;
}
return TryMarkReadyIfPresent( key );
}
public static void BumpPriority( string soundId )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return;
EnsureInitialized();
var key = Normalize( soundId );
if ( IsReady( key ) )
return;
if ( _states.TryGetValue( key, out var state ) )
{
if ( state is LoadState.Loading or LoadState.Ready )
return;
if ( state == LoadState.Failed )
_states[key] = LoadState.Pending;
}
else
{
_states[key] = LoadState.Pending;
_registered++;
}
_priority.Remove( key );
_priority.Insert( 0, key );
Changed?.Invoke();
}
public static void QueuePlayWhenReady( SoundPad pad )
{
if ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )
return;
var key = Normalize( pad.SoundId );
_pendingPlays[key] = pad;
PendingRevision++;
BumpPriority( key );
}
public static bool TryTakePendingPlay( string soundId, out SoundPad pad )
{
var removed = _pendingPlays.Remove( Normalize( soundId ), out pad );
if ( removed )
PendingRevision++;
return removed;
}
public static bool IsPendingPlay( string soundId )
{
return _pendingPlays.ContainsKey( Normalize( soundId ) );
}
public static void Tick()
{
EnsureInitialized();
TryPumpLoads();
}
static void TryPumpLoads()
{
while ( _activeLoads < MaxConcurrentLoads && TryDequeue( out var soundId ) )
{
if ( TryMarkReadyIfPresent( soundId ) )
{
SoundBoardManager.OnSoundReady( soundId );
continue;
}
_activeLoads++;
_states[soundId] = LoadState.Loading;
_ = LoadOneAsync( soundId );
}
}
static bool TryDequeue( out string soundId )
{
while ( _priority.Count > 0 )
{
var next = _priority[0];
_priority.RemoveAt( 0 );
if ( !_states.TryGetValue( next, out var st ) || st != LoadState.Pending )
continue;
soundId = next;
return true;
}
soundId = null;
return false;
}
static async Task LoadOneAsync( string soundId )
{
var path = ToSoundPath( soundId );
try
{
if ( ResourceLibrary.TryGet<SoundEvent>( path, out _ ) )
{
MarkReady( soundId );
SoundBoardManager.OnSoundReady( soundId );
return;
}
var loaded = await ResourceLibrary.LoadAsync<SoundEvent>( path );
if ( loaded is null )
{
MarkFailed( soundId );
return;
}
try { Sound.Preload( path ); }
catch { /* playable without preload */ }
MarkReady( soundId );
SoundBoardManager.OnSoundReady( soundId );
}
catch ( Exception e )
{
Log.Warning( $"[S&Soundboard] Sound load skipped: {path} ({e.GetType().Name})" );
MarkFailed( soundId );
}
finally
{
_activeLoads = Math.Max( 0, _activeLoads - 1 );
}
}
static bool TryMarkReadyIfPresent( string key )
{
try
{
if ( !ResourceLibrary.TryGet<SoundEvent>( ToSoundPath( key ), out _ ) )
return false;
}
catch
{
return false;
}
MarkReady( key );
return true;
}
static void MarkReady( string key )
{
var already = _states.TryGetValue( key, out var prev ) && prev == LoadState.Ready;
_states[key] = LoadState.Ready;
if ( !already )
_loaded++;
Changed?.Invoke();
}
static void MarkFailed( string key )
{
_states[key] = LoadState.Failed;
if ( _pendingPlays.Remove( key ) )
PendingRevision++;
Changed?.Invoke();
}
static string Normalize( string soundId )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return soundId;
soundId = soundId.Replace( '\\', '/' ).Trim();
return soundId.EndsWith( ".sound", StringComparison.OrdinalIgnoreCase )
? soundId[..^6]
: soundId;
}
static string ToSoundPath( string soundId ) => $"{Normalize( soundId )}.sound";
}