Files
sbox-public/engine/Sandbox.Engine/Game/Services/Stats/Stats.Player.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

301 lines
6.6 KiB
C#

using System.Collections;
namespace Sandbox.Services;
public static partial class Stats
{
/// <summary>
/// Static cache of all the global stats
/// </summary>
static CaseInsensitiveDictionary<PlayerStats> _playerStats = new CaseInsensitiveDictionary<PlayerStats>();
/// <summary>
/// Get the global stats for the calling package
/// </summary>
public static PlayerStats LocalPlayer
{
get
{
var package = Application.GameIdent;
if ( package is null ) return default;
return GetLocalPlayerStats( package );
}
}
/// <summary>
/// Get the global stats for this package
/// </summary>
public static PlayerStats GetLocalPlayerStats( string packageIdent ) => GetPlayerStats( packageIdent, (long)Steamworks.SteamClient.SteamId.Value );
/// <summary>
/// Get the stats for this package
/// </summary>
public static PlayerStats GetPlayerStats( string packageIdent, long steamid )
{
var hash = $"{packageIdent}/{steamid}".ToLowerInvariant();
if ( _playerStats.TryGetValue( hash, out var s ) )
return s;
s = new PlayerStats( packageIdent, steamid );
_ = s.Refresh();
_playerStats[hash] = s;
return s;
}
public sealed class PlayerStats : IEnumerable<PlayerStat>
{
private string _packageIdent;
private long _steamId;
private Dictionary<string, PlayerStat> stats = new Dictionary<string, PlayerStat>( StringComparer.OrdinalIgnoreCase );
/// <summary>
/// True if we're currently fetching new stats
/// </summary>
public bool IsRefreshing { get; private set; }
/// <summary>
/// The UTC datetime when we last fetched new stats
/// </summary>
public DateTime LastRefresh { get; private set; }
internal PlayerStats( string packageIdent, long steamId )
{
_packageIdent = packageIdent;
_steamId = steamId;
}
/// <summary>
/// Make a copy of this class. Allows you to store the stats from a point in time.
/// </summary>
public PlayerStats Copy()
{
var pc = new PlayerStats( _packageIdent, _steamId );
pc.LastRefresh = LastRefresh;
foreach ( var (k, v) in stats )
{
pc.stats.Add( k, v );
}
return pc;
}
/// <summary>
/// Get a stat by name. Will return an empty stat if not found
/// </summary>
public PlayerStat Get( string name )
{
if ( TryGet( name, out var stat ) )
return stat;
return default;
}
/// <summary>
/// Get a stat by name, returns true if found
/// </summary>
public bool TryGet( string name, out PlayerStat stat )
{
return stats.TryGetValue( name, out stat );
}
/// <summary>
/// Refresh these global stats - grab the latest values
/// </summary>
public async Task Refresh()
{
if ( Backend.Stats is null )
return;
// if we're refreshing, wait for it to finish and return
while ( IsRefreshing )
{
while ( IsRefreshing )
await Task.Delay( 100 );
return;
}
// Don't allow refreshing so often
if ( LastRefresh > DateTime.UtcNow.AddSeconds( -10 ) )
return;
if ( _packageIdent.StartsWith( "local." ) )
return;
try
{
IsRefreshing = true;
var stats = await Backend.Stats.GetPlayerPackageStats( _packageIdent, _steamId );
if ( stats is null ) return;
this.stats.Clear();
foreach ( var entry in stats )
{
this.stats[entry.Name] = new PlayerStat( entry );
}
LastRefresh = DateTime.UtcNow;
}
catch ( System.Exception e )
{
if ( !Application.IsEditor )
{
Log.Warning( e, "Exception when fetching player stats" );
}
}
finally
{
IsRefreshing = false;
}
}
public IEnumerator<PlayerStat> GetEnumerator() => stats.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => stats.Values.GetEnumerator();
/// <summary>
/// When the stat is queued for change on the backend, we can predict its change on the front
/// end, for achievements etc.
/// </summary>
internal void Predict( string name, double amount )
{
var stat = Get( name );
stat = new PlayerStat
{
Name = name,
Title = stat.Title,
Description = stat.Description,
Unit = stat.Unit,
Value = stat.Value,
ValueString = stat.ValueString,
Max = Math.Max( stat.Max, amount ),
Min = Math.Min( stat.Min, amount ),
Sum = stat.Sum + amount,
Last = DateTime.UtcNow,
LastValue = amount,
First = stat.First,
FirstValue = stat.FirstValue
};
stats[name] = stat;
}
public PlayerStat this[string propertyName]
{
get => Get( propertyName );
}
}
[Expose]
public readonly struct PlayerStat
{
internal PlayerStat( Sandbox.Services.PlayerStat entry )
{
Name = entry.Name;
Title = entry.Title;
Description = entry.Description;
Unit = entry.Unit;
Value = entry.Value;
ValueString = entry.ValueString;
Max = entry.Max;
Avg = entry.Avg;
Min = entry.Min;
Sum = entry.Sum;
Last = entry.Last;
LastValue = entry.LastValue;
First = entry.First;
FirstValue = entry.FirstValue;
}
/// <summary>
/// The programatic name for this stat. This should probably be called Ident
/// </summary>
public string Name { get; init; }
/// <summary>
/// The title of this stat, as defined on the backend
/// </summary>
public string Title { get; init; }
/// <summary>
/// The description of this stat, as defined on the backend
/// </summary>
public string Description { get; init; }
/// <summary>
/// The unit of this stat as defined on the backend
/// </summary>
public string Unit { get; init; }
/// <summary>
/// The current stat value
/// </summary>
public double Value { get; init; }
/// <summary>
/// The current value formatted using Unit
/// </summary>
public string ValueString { get; init; }
/// <summary>
/// The maximum value
/// </summary>
public double Max { get; init; }
/// <summary>
/// The minimum value
/// </summary>
public double Min { get; init; }
/// <summary>
/// The average value
/// </summary>
public double Avg { get; init; }
/// <summary>
/// The sum of all values
/// </summary>
public double Sum { get; init; }
/// <summary>
/// The time of the last value
/// </summary>
public DateTimeOffset Last { get; init; }
/// <summary>
/// The last value
/// </summary>
public double LastValue { get; init; }
/// <summary>
/// The time of the first value
/// </summary>
public DateTimeOffset First { get; init; }
/// <summary>
/// The last value
/// </summary>
public double FirstValue { get; init; }
internal readonly double GetValue( AggregationType type )
{
return type switch
{
AggregationType.Highest => Max,
AggregationType.Lowest => Min,
AggregationType.Latest => LastValue,
AggregationType.Median => Avg,
_ => Sum,
};
}
}
}