using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace game.PlayerDataStorage { internal enum PlayerKVShutdownStatus { Ready, Pending, Failed, Cancelled, Input } /// Process-lifetime barrier. Faulted and unfinished work cannot be reset by replacing a scene/container. internal sealed class PlayerKVShutdownRegistry { internal static PlayerKVShutdownRegistry Process { get; } = new PlayerKVShutdownRegistry(); readonly object gate = new object(); readonly Dictionary pending = new Dictionary(); TaskCompletionSource changed = Signal(); internal void Register(Task task, PlayerKVLoginContext owner = null) { if (task == null) throw new ArgumentNullException(nameof(task)); lock (gate) { if (!pending.ContainsKey(task) || owner != null) pending[task] = owner; NotifyChanged(); } _ = task.ContinueWith(t => { if (t.IsFaulted) { _ = t.Exception; } lock (gate) { if (t.Status == TaskStatus.RanToCompletion) pending.Remove(t); NotifyChanged(); } // Cancelled work does not prove resource cleanup; retain it as a failure barrier. }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } internal PlayerKVShutdownStatus Status { get { lock (gate) { if (pending.Keys.Any(t => t.IsFaulted || t.IsCanceled)) return PlayerKVShutdownStatus.Failed; return pending.Keys.Any(t => !t.IsCompleted) ? PlayerKVShutdownStatus.Pending : PlayerKVShutdownStatus.Ready; } } } // Injection is an observation clock only, never a force-close or permission to reopen. internal async Task ObserveAsync(int timeoutMs, CancellationToken token = default, Func delay = null) { if (timeoutMs < 1000 || timeoutMs > 15000) return PlayerKVShutdownStatus.Input; if (token.IsCancellationRequested) return PlayerKVShutdownStatus.Cancelled; using (var clock = CancellationTokenSource.CreateLinkedTokenSource(token)) { var deadline = (delay ?? Task.Delay)(timeoutMs, clock.Token); try { while (true) { if (token.IsCancellationRequested) return PlayerKVShutdownStatus.Cancelled; Task notification; lock (gate) { var status = Status; if (status != PlayerKVShutdownStatus.Pending) return status; notification = changed.Task; } if (await Task.WhenAny(notification, deadline).ConfigureAwait(false) == deadline) { if (token.IsCancellationRequested) return PlayerKVShutdownStatus.Cancelled; return Status; } // Recheck the live collection; another Root may have registered work during the await. } } finally { clock.Cancel(); } } } void NotifyChanged() { var previous = changed; changed = Signal(); previous.TrySetResult(true); } static TaskCompletionSource Signal() => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } }