Core/GameProfile.PendingRuns.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Prototype;

public sealed class PendingRunSubmission
{
    public RunRecord Record { get; set; }
    public bool PersonalBest { get; set; }
}

public static partial class GameProfile
{
    private const string PendingRunsKey = "dasher.pending-runs.v1";
    private static List<PendingRunSubmission> _pendingRuns = new();
    private static bool _sendingRuns;
    private static DateTime _retryRunsAt;

    private static void LoadPendingRuns() => _pendingRuns = Sandbox.Game.Cookies.Get<List<PendingRunSubmission>>(PendingRunsKey, null) ?? new();
    private static void SavePendingRuns() => Sandbox.Game.Cookies.Set(PendingRunsKey, _pendingRuns);

    private static void QueueRun(RunRecord record, bool personalBest)
    {
        foreach (var pending in _pendingRuns)
            if (!string.IsNullOrEmpty(record.RunId) && pending.Record.RunId == record.RunId) return;
        _pendingRuns.Add(new PendingRunSubmission { Record = record, PersonalBest = personalBest });
        SavePendingRuns();
        RetryPendingRuns();
    }

    public static void RetryPendingRuns()
    {
        if (!_loaded || _sendingRuns || _pendingRuns.Count == 0 || DateTime.UtcNow < _retryRunsAt) return;
        _sendingRuns = true;
        _ = SendPendingRuns();
    }

    private static async Task SendPendingRuns()
    {
        try
        {
            // FIFO keeps last-score stats and their metadata in run order.
            while (_pendingRuns.Count > 0)
            {
                var pending = _pendingRuns[0];
                var record = pending.Record;
                var key = record.Category.ToLowerInvariant();
                var metadata = new Dictionary<string, object> { ["team"] = record.PlayerName, ["run"] = record.RunId, ["kills"] = record.Kills, ["levels"] = record.Levels, ["seconds"] = record.Seconds };
                Sandbox.Services.Stats.SetValue($"dasher_{key}_team_last_score", record.Score, metadata);
                if (pending.PersonalBest)
                {
                    Sandbox.Services.Stats.SetValue($"prototype_{key}_score", record.Score, metadata);
                    Sandbox.Services.Stats.SetValue($"prototype_{key}_kills", record.Kills);
                    Sandbox.Services.Stats.SetValue($"prototype_{key}_levels", record.Levels);
                    Sandbox.Services.Stats.SetValue($"prototype_{key}_time", (long)MathF.Ceiling(record.Seconds));
                }
                using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
                await Sandbox.Services.Stats.FlushAndWaitAsync(timeout.Token);
                _pendingRuns.RemoveAt(0);
                SavePendingRuns();
            }
        }
        catch (Exception exception)
        {
            // Retry SetValue, never Increment: a lost acknowledgement must not add coins/kills.
            Log.Warning($"Run results saved locally; upload will retry: {exception.Message}");
        }
        finally
        {
            _retryRunsAt = DateTime.UtcNow.AddSeconds(30);
            _sendingRuns = false;
        }
    }
}