Tracks play counts for sound pads, persists them to local Game.Cookies, builds a trending list from counts, and submits a simple global stat for each play.
using System.Collections.Generic;
using System.Linq;
using Sandbox.Services;
namespace SSoundboard;
/// <summary>
/// Tracks how often each sound pad is pressed and persists counts locally.
/// Trending is built from the highest counts across the whole soundboard.
/// </summary>
public static class SoundPlayTracker
{
const string CookieKey = "ssoundboard.pad-plays";
const int TrendingCount = 15;
static readonly Dictionary<string, int> _plays = new( StringComparer.OrdinalIgnoreCase );
static bool _loaded;
public static event Action Changed;
public static void EnsureLoaded()
{
if ( _loaded )
return;
var saved = Game.Cookies.Get( CookieKey, new Dictionary<string, int>() );
_plays.Clear();
foreach ( var (soundId, count) in saved )
{
if ( count > 0 )
_plays[soundId] = count;
}
_loaded = true;
}
public static int GetCount( string soundId )
{
EnsureLoaded();
return string.IsNullOrWhiteSpace( soundId ) ? 0 : _plays.GetValueOrDefault( soundId );
}
public static bool HasAnyPlays()
{
EnsureLoaded();
return _plays.Count > 0;
}
public static void Record( string soundId )
{
if ( string.IsNullOrWhiteSpace( soundId ) )
return;
EnsureLoaded();
_plays[soundId] = _plays.GetValueOrDefault( soundId ) + 1;
Game.Cookies.Set( CookieKey, _plays );
SubmitGlobalStat( soundId );
Changed?.Invoke();
}
public static IReadOnlyList<SoundPad> BuildTrendingPads(
IReadOnlyList<SoundPad> pool,
IReadOnlyList<SoundPad> fallback )
{
EnsureLoaded();
if ( !HasAnyPlays() )
return fallback.ToList();
var ranked = pool
.OrderByDescending( pad => GetCount( pad.SoundId ) )
.ThenBy( pad => pad.Label )
.Take( TrendingCount )
.ToList();
if ( ranked.Count >= TrendingCount )
return ranked;
foreach ( var pad in fallback )
{
if ( ranked.Any( x => x.SoundId.Equals( pad.SoundId, StringComparison.OrdinalIgnoreCase ) ) )
continue;
ranked.Add( pad );
if ( ranked.Count >= TrendingCount )
break;
}
return ranked;
}
static void SubmitGlobalStat( string soundId )
{
try
{
Stats.Map.SetValue( ToStatName( soundId ), 1.0, null );
}
catch
{
// Global stats are optional until package stats are configured on sbox.game.
}
}
static string ToStatName( string soundId )
{
var slug = soundId
.Replace( "sounds/", "", StringComparison.OrdinalIgnoreCase )
.Replace( '/', '.' )
.Replace( ' ', '_' )
.ToLowerInvariant();
return $"pad.{slug}";
}
}