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;
}
}
}