保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
200 lines
11 KiB
C#
200 lines
11 KiB
C#
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<string, PlayerKVSyncTicket> pendingReceipts = new Dictionary<string, PlayerKVSyncTicket>(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<PlayerSaveConfirmation> 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<PlayerSaveConfirmation> 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<PlayerSaveConfirmation> 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<PlayerSaveConfirmation> 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<bool>(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<PlayerSaveConfirmation> 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<PlayerSaveConfirmation> 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));
|
|
}
|
|
}
|
|
}
|