UI/LeaderboardData.cs
using System.Threading.Tasks;
namespace Monolith;
/// <summary>
/// Fetches one global leaderboard and flattens it into plain rows for the HUD.
///
/// All the s&box-typed leaderboard code is deliberately confined to <see cref="Load"/> and
/// uses <c>var</c>, so if the API surface shifts there is exactly one method to correct rather
/// than a UI full of engine types.
/// </summary>
public sealed class LeaderboardData
{
public struct Row
{
public int Rank;
public string Name;
/// <summary>Formatted for display.</summary>
public string Value;
/// <summary>
/// The unformatted number. Kept alongside the string so callers can COMPARE against a
/// board without reparsing "2:05.3", which is the kind of round trip that quietly breaks
/// the first time a format changes.
/// </summary>
public double Raw;
public bool IsMe;
}
public string Title { get; }
public string StatName { get; }
/// <summary>Speedrun boards rank by lowest time, and format seconds rather than counts.</summary>
public bool LowestWins { get; init; }
public List<Row> Rows { get; } = new();
public bool Loading { get; private set; }
public string Error { get; private set; }
public bool HasLoaded { get; private set; }
/// <summary>Bumped on every state change so the HUD's BuildHash notices.</summary>
public int Revision { get; private set; }
public LeaderboardData( string title, string statName )
{
Title = title;
StatName = statName;
}
public async Task Load( int maxEntries = 15 )
{
if ( Loading ) return;
Loading = true;
Error = null;
Revision++;
try
{
var board = Sandbox.Services.Leaderboards.GetFromStat( MonolithStats.PackageIdent, StatName );
board.MaxEntries = maxEntries;
if ( LowestWins )
{
// A speedrun board wants the smallest submitted time, ordered ascending.
board.SetAggregationMin();
board.SetSortAscending();
}
await board.Refresh();
Rows.Clear();
// Compared as text so it does not matter whether the engine types these as
// long or ulong, which differ between Connection and the leaderboard entry.
var mySteamId = Connection.Local?.SteamId.ToString();
foreach ( var entry in board.Entries )
{
Rows.Add( new Row
{
Rank = (int)entry.Rank,
Name = string.IsNullOrWhiteSpace( entry.DisplayName ) ? "unknown" : entry.DisplayName,
Value = LowestWins
? Num.Duration( (double)entry.Value )
: Num.Short( (double)entry.Value ),
Raw = (double)entry.Value,
IsMe = mySteamId != null && entry.SteamId.ToString() == mySteamId,
} );
}
HasLoaded = true;
}
catch ( Exception e )
{
Error = e.Message;
Log.Warning( $"Leaderboard '{StatName}' failed to load: {e.Message}" );
}
finally
{
Loading = false;
Revision++;
}
}
}