using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace game.PlayerDataStorage { public sealed class KVPlayerSaveStore : IPlayerSaveStore, IDisposable { PlayerKVGameSessionHandle handle; CancellationToken lifetime; bool bound, disposed; readonly object receiptsGate = new object(); readonly Dictionary pendingReceipts = new Dictionary(StringComparer.Ordinal); public bool IsCurrent => bound && !disposed && !lifetime.IsCancellationRequested && handle.Status != PlayerKVSyncStatus.Closed; internal void Bind(PlayerKVGameSessionHandle original, CancellationToken loginLifetime) { if (bound || disposed || original == null || loginLifetime.IsCancellationRequested) throw new InvalidOperationException("Player save store cannot be rebound."); // This also checks the load guard before permitting business use. if (!KVMgr.ReadKV(original, "LastLoginTimeUtc").Success) throw new InvalidOperationException("Player save handle is not ready."); handle = original; lifetime = loginLifetime; bound = true; } public PlayerSaveReadResult Read(string key) { if (!IsCurrent) return new PlayerSaveReadResult("StaleOrNotReady"); var result = KVMgr.ReadKV(handle, key); return result.Success ? new PlayerSaveReadResult(result.Found.Value, result.Value) : new PlayerSaveReadResult(result.Error.ToString()); } public PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true) { lock (receiptsGate) { if (!IsCurrent) { var rejected = new PlayerSaveWriteResult("StaleOrNotReady"); return rejected; } var result = KVMgr.WriteKV(handle, key, value, remoteStore); if (result.Accepted && result.Ticket != null) pendingReceipts[key] = result.Ticket; var write = result.Accepted ? new PlayerSaveWriteResult(new PlayerSaveReceipt(this, result.Ticket)) : new PlayerSaveWriteResult(result.StateResult.Error.ToString()); return write; } } public async Task WriteAndConfirmAsync(string key, string value, CancellationToken token = default) { if (token.IsCancellationRequested) return Cancelled(); var write = Write(key, value); if (!write.Accepted) return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, write.Error); return await ConfirmAsync(write.Receipt, token); } public async Task ConfirmAsync(PlayerSaveReceipt receipt, CancellationToken token = default) { if (!IsCurrent || token.IsCancellationRequested) return Cancelled(); if (receipt == null || !ReferenceEquals(receipt.Store, this)) return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, "ForeignReceipt"); if (receipt.Ticket == null) return new PlayerSaveConfirmation(PlayerSaveStatus.LocalOnly); using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token)) { try { await KVMgr.FlushKVAsync(handle, wait.Token); var ticket = await KVMgr.WaitForKVTicketAsync(handle, (PlayerKVSyncTicket)receipt.Ticket, wait.Token); if (!IsCurrent || wait.IsCancellationRequested) return Cancelled(); if (ticket.Status == PlayerKVTicketStatus.Confirmed) { ForgetConfirmed((PlayerKVSyncTicket)receipt.Ticket); return new PlayerSaveConfirmation(PlayerSaveStatus.Confirmed); } return new PlayerSaveConfirmation(ticket.Status == PlayerKVTicketStatus.Unknown ? PlayerSaveStatus.Unknown : ticket.Status == PlayerKVTicketStatus.Superseded ? PlayerSaveStatus.Superseded : ticket.Status == PlayerKVTicketStatus.Cancelled ? PlayerSaveStatus.Cancelled : PlayerSaveStatus.Rejected, ticket.TransportError.ToString()); } catch (OperationCanceledException) { return Cancelled(); } } } public async Task FlushAsync(CancellationToken token = default) { if (!IsCurrent || token.IsCancellationRequested) return Cancelled(); PlayerKVSyncTicket[] captured; lock (receiptsGate) captured = pendingReceipts.Values.ToArray(); using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token)) { try { var result = await KVMgr.FlushKVAsync(handle, wait.Token); if (!IsCurrent || wait.IsCancellationRequested) return Cancelled(); var status = result.Unknown > 0 || result.CacheStatus == PlayerKVSyncStatus.UnknownOutcome ? PlayerSaveStatus.Unknown : result.Blocked > 0 || result.TransportError != PlayerKVError.None || result.CacheStatus != PlayerKVSyncStatus.Ready ? PlayerSaveStatus.Rejected : PlayerSaveStatus.Confirmed; if (status != PlayerSaveStatus.Confirmed) return new PlayerSaveConfirmation(status, result.TransportError.ToString()); // Multiple writes before this flush are covered by the captured final receipt. // A replacement during the await belongs to a later flush and cannot confirm it. foreach (var receipt in captured) { var ticket = await KVMgr.WaitForKVTicketAsync(handle, receipt, wait.Token); if (!IsCurrent || wait.IsCancellationRequested) return Cancelled(); if (ticket.Status != PlayerKVTicketStatus.Confirmed) return new PlayerSaveConfirmation(ticket.Status == PlayerKVTicketStatus.Superseded ? PlayerSaveStatus.Superseded : ticket.Status == PlayerKVTicketStatus.Unknown ? PlayerSaveStatus.Unknown : ticket.Status == PlayerKVTicketStatus.Cancelled ? PlayerSaveStatus.Cancelled : PlayerSaveStatus.Rejected, ticket.TransportError.ToString()); ForgetConfirmed(receipt); } return new PlayerSaveConfirmation(PlayerSaveStatus.Confirmed); } catch (OperationCanceledException) { return Cancelled(); } } } static PlayerSaveConfirmation Cancelled() => new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled); void ForgetConfirmed(PlayerKVSyncTicket receipt) { lock (receiptsGate) if (pendingReceipts.TryGetValue(receipt.Key, out var latest) && ReferenceEquals(latest, receipt)) pendingReceipts.Remove(receipt.Key); } public void Dispose() { disposed = true; lock (receiptsGate) { pendingReceipts.Clear(); } } } public sealed class PlayFabPlayerSaveStore : IPlayerSaveStore { readonly PlayFabMgr manager; readonly CancellationToken lifetime; public PlayFabPlayerSaveStore(IUserService user) : this(PlayFabMgr.Instance, user.LoginLifetime) { } internal PlayFabPlayerSaveStore(PlayFabMgr manager, CancellationToken lifetime) { this.manager = manager; this.lifetime = lifetime; } public bool IsCurrent => manager != null && !lifetime.IsCancellationRequested; public PlayerSaveReadResult Read(string key) { if (!IsCurrent) return new PlayerSaveReadResult("StaleOrNotReady"); try { var value = manager.GetLocalData(key); return new PlayerSaveReadResult(value != null, value); } catch (Exception ex) { return new PlayerSaveReadResult(ex.GetType().Name); } } public PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true) { if (!IsCurrent) return new PlayerSaveWriteResult("StaleOrNotReady"); try { manager.UpdateUserDataValue(key, value, remoteStore); return new PlayerSaveWriteResult(new PlayerSaveReceipt(this, remoteStore ? (object)true : null)); } catch (Exception ex) { return new PlayerSaveWriteResult(ex.GetType().Name); } } public async Task WriteAndConfirmAsync(string key, string value, CancellationToken token = default) { if (!IsCurrent || token.IsCancellationRequested) return new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled); using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token)) { var cancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using (wait.Token.Register(() => cancelled.TrySetResult(false))) { try { var save = manager.SyncDataImmidiate(key, value); var finished = await Task.WhenAny(save, cancelled.Task); if (!IsCurrent || wait.IsCancellationRequested) return new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled); return new PlayerSaveConfirmation(await finished ? PlayerSaveStatus.Confirmed : PlayerSaveStatus.Rejected); } catch (Exception ex) { return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, ex.GetType().Name); } } } } public Task ConfirmAsync(PlayerSaveReceipt receipt, CancellationToken token = default) => Task.FromResult(new PlayerSaveConfirmation(!IsCurrent || token.IsCancellationRequested ? PlayerSaveStatus.Cancelled : receipt == null || !ReferenceEquals(receipt.Store, this) ? PlayerSaveStatus.Rejected : receipt.Ticket == null ? PlayerSaveStatus.LocalOnly : PlayerSaveStatus.Buffered)); public Task FlushAsync(CancellationToken token = default) { if (!IsCurrent || token.IsCancellationRequested) return Task.FromResult(new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled)); // The original manager owns its timer and callback-based buffered dispatch. manager.FlushBufferedData(); return Task.FromResult(new PlayerSaveConfirmation(PlayerSaveStatus.Buffered)); } } }