Game/Leaderboard.cs

Static leaderboard helper for the DesertPump game. Submits a "tallest_tree" stat to the Sandbox stat service, fetches the global leaderboard via Sandbox Leaderboards, and exposes rows, status text, and a fetching flag. It always ensures at least the local player appears on the board.

Networking
using System.Threading.Tasks;
using Sandbox.Services;

namespace DesertPump;

/// <summary>
/// The tallest-tree board.
/// </summary>
/// <remarks>
/// The global board is real - it submits to s&amp;box's stat service under
/// <see cref="StatName"/> and reads the board back. It only has anything in it once the
/// game is published to sbox.game with a leaderboard configured; running locally the
/// backend has no rows to give, so <see cref="Entries"/> comes back empty and the panel
/// says so rather than pretending.
///
/// Your own height is tracked locally either way, so the board always shows you.
/// </remarks>
public static class Leaderboard
{
	public const string StatName = "tallest_tree";

	public sealed class Row
	{
		public int Rank { get; init; }
		public string Name { get; init; }
		public int Height { get; init; }
		public bool IsMe { get; init; }
	}

	/// <summary>Rows from the last fetch, best first. Empty until one succeeds.</summary>
	public static List<Row> Entries { get; private set; } = new();

	/// <summary>Null until we've tried, then a short human-readable status.</summary>
	public static string Status { get; private set; }

	public static bool Fetching { get; private set; }

	/// <summary>Push the height up as a stat. Safe to call often - the service batches.</summary>
	public static void Submit( int height )
	{
		try
		{
			Stats.SetValue( StatName, height );
		}
		catch ( Exception e )
		{
			// No backend when running locally - not worth spamming the console over.
			Status = $"Couldn't submit: {e.Message}";
		}
	}

	/// <summary>Pull the global board. Falls back to a board of one - you.</summary>
	public static async Task Refresh( int myHeight, string myName )
	{
		if ( Fetching ) return;

		Fetching = true;

		try
		{
			var board = Leaderboards.Get( StatName );
			board.MaxEntries = 20;

			await board.Refresh();

			var rows = new List<Row>();
			foreach ( var entry in board.Entries )
			{
				rows.Add( new Row
				{
					Rank = (int)entry.Rank,
					Name = entry.DisplayName,
					Height = (int)entry.Value,
					IsMe = entry.Me
				} );
			}

			Entries = rows;
			Status = rows.Count > 0
				? null
				: "No global scores yet - publish the game to sbox.game to open the board up.";
		}
		catch ( Exception e )
		{
			Entries = new List<Row>();
			Status = $"Global board unavailable ({e.Message})";
		}
		finally
		{
			// However it went, you should always see your own tree on the board.
			if ( Entries.Count == 0 )
			{
				Entries = new List<Row>
				{
					new() { Rank = 1, Name = myName, Height = myHeight, IsMe = true }
				};
			}

			Fetching = false;
		}
	}
}