SoundPlayTracker.cs

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.

File AccessNetworking
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}";
	}
}