feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using game.Account;
|
||||
@@ -39,23 +40,50 @@ public class CustomServerMgr : ICustomServerMgr
|
||||
|
||||
private CookieContainer _cookie;
|
||||
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
||||
private readonly SynchronizationContext ownerContext;
|
||||
public int SessionVersion { get; private set; }
|
||||
public event Action SessionChanged;
|
||||
PlayerKVAccountLease kvLease;
|
||||
bool kvOnly;
|
||||
bool kvBusinessReady;
|
||||
bool kvBusinessPublished;
|
||||
int pfRequests;
|
||||
internal event Action<int> KVUnauthorized;
|
||||
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
|
||||
internal void BindKVForValidation(PlayerKVAccountLease lease)
|
||||
{
|
||||
if(lease==null || !lease.IsCurrent || lease.ApiBaseUri!=ApiBaseUri || !IsFreshForKVLoad)
|
||||
if(lease==null || lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri!=ApiBaseUri || !IsFreshForKVLoad)
|
||||
throw new InvalidOperationException("Fresh matching KV lease required.");
|
||||
kvOnly=true; SessionVersion++; kvLease=lease; _cookie=null;
|
||||
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
||||
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
||||
}
|
||||
internal void BindKVForGame(PlayerKVAccountLease lease)
|
||||
{
|
||||
if(lease==null || !lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
|
||||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl || !IsFreshForKVLoad)
|
||||
throw new InvalidOperationException("Fresh formal KV lease required.");
|
||||
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
||||
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
||||
}
|
||||
internal void EnableKVBusiness(long epoch)
|
||||
{
|
||||
if(!kvOnly || kvBusinessReady || kvBusinessPublished || kvLease==null || !kvLease.IsFormal || !kvLease.IsCurrent ||
|
||||
kvLease.Epoch!=epoch || kvLease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
|
||||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl)
|
||||
throw new InvalidOperationException("Current formal KV lease required for business transport.");
|
||||
kvBusinessPublished=true; kvBusinessReady=true;
|
||||
}
|
||||
internal void DisableKVBusiness()
|
||||
{
|
||||
if(!kvOnly || !kvBusinessReady) return;
|
||||
kvBusinessReady=false; SessionVersion++;
|
||||
try { SessionChanged?.Invoke(); }
|
||||
catch(Exception ex) { Debug.LogError($"KV session change failed: {ex.GetType().Name}"); }
|
||||
}
|
||||
internal Task ClearKVForGame() => ClearKVForValidation();
|
||||
internal Task ClearKVForValidation()
|
||||
{
|
||||
SessionVersion++; var previous=kvLease; kvLease=null;
|
||||
kvBusinessReady=false; SessionVersion++; var previous=kvLease; kvLease=null; _cookie=null;
|
||||
var closing=previous?.CloseAsync() ?? Task.CompletedTask;
|
||||
SessionChanged?.Invoke(); return closing;
|
||||
}
|
||||
@@ -74,13 +102,16 @@ public class CustomServerMgr : ICustomServerMgr
|
||||
}
|
||||
|
||||
|
||||
public CookieContainer Cookie => _cookie ?? throw new System.NullReferenceException();
|
||||
public CookieContainer Cookie => kvOnly
|
||||
? (kvBusinessReady && kvLease?.IsFormal==true ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
|
||||
: _cookie ?? throw new NullReferenceException();
|
||||
public Uri ApiBaseUri { get; }
|
||||
|
||||
public CustomServerMgr(IConfig config) : this(config, null) { }
|
||||
|
||||
public CustomServerMgr(IConfig config, Func<CookieContainer, HttpMessageHandler> handlerFactory)
|
||||
{
|
||||
ownerContext=SynchronizationContext.Current;
|
||||
this.handlerFactory = handlerFactory ?? (cookies => new HttpClientHandler
|
||||
{
|
||||
CookieContainer = cookies, UseCookies = true, AllowAutoRedirect = false
|
||||
@@ -125,7 +156,42 @@ public class CustomServerMgr : ICustomServerMgr
|
||||
// Unity callers use the main thread. Capture a fixed transport before the first await.
|
||||
private async Task<string> SendAsync(HttpMethod method, string url, string body, string contentType)
|
||||
{
|
||||
if(kvOnly) throw new InvalidOperationException("Only controlled validation routes are enabled.");
|
||||
if(kvOnly)
|
||||
{
|
||||
if(ownerContext!=null && !ReferenceEquals(SynchronizationContext.Current,ownerContext))
|
||||
{
|
||||
var completion=new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
ownerContext.Post(async _=>
|
||||
{
|
||||
try { completion.TrySetResult(await SendAsync(method,url,body,contentType)); }
|
||||
catch(Exception ex) { completion.TrySetException(ex); }
|
||||
},null);
|
||||
return await completion.Task.ConfigureAwait(false);
|
||||
}
|
||||
if(!kvBusinessReady) throw new InvalidOperationException("Formal KV business transport is not ready.");
|
||||
var lease=kvLease; var kvVersion=SessionVersion;
|
||||
if(lease==null || !lease.IsFormal || !lease.IsCurrent) return null;
|
||||
try
|
||||
{
|
||||
var response=await lease.SendAsync(method,url,body,contentType);
|
||||
if(!kvBusinessReady || kvVersion!=SessionVersion || !ReferenceEquals(lease,kvLease) || !response.IsCurrent)
|
||||
return null;
|
||||
if(response.StatusCode==(int)HttpStatusCode.OK) return response.Body;
|
||||
if(response.StatusCode==(int)HttpStatusCode.Unauthorized || response.StatusCode==(int)HttpStatusCode.Forbidden)
|
||||
{
|
||||
DisableKVBusiness();
|
||||
KVUnauthorized?.Invoke(SessionVersion);
|
||||
}
|
||||
else if(response.StatusCode.HasValue)
|
||||
Debug.LogError($"API request failed: HTTP {response.StatusCode.Value}");
|
||||
return null;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if(kvVersion==SessionVersion) Debug.LogError($"API request failed: {e.GetType().Name}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
pfRequests++;
|
||||
var transport = client;
|
||||
var version = SessionVersion;
|
||||
|
||||
52
Client/Assets/Scripts/Core/KVMgr.cs
Normal file
52
Client/Assets/Scripts/Core/KVMgr.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using game.PlayerDataStorage;
|
||||
|
||||
internal static class KVMgr
|
||||
{
|
||||
internal static PlayerKVKeyReadResult ReadKV(PlayerKVGameSessionHandle handle, string key)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.ReadLocal))
|
||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.LoadGuardActive, handle);
|
||||
if (handle == null || !KVSnapshot.ValidKey(key))
|
||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.InvalidInput, handle);
|
||||
var read = handle.ReadLocalSnapshot();
|
||||
if (!read.Success)
|
||||
return new PlayerKVKeyReadResult(read.Error == PlayerKVLocalReadError.StaleOrClosing
|
||||
? PlayerKVKeyReadError.StaleOrClosing : PlayerKVKeyReadError.NotReady, handle);
|
||||
var found = read.Data.TryGetValue(key, out var value);
|
||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.None, handle, found, value);
|
||||
}
|
||||
|
||||
internal static PlayerKVEditResult WriteKV(PlayerKVGameSessionHandle handle,
|
||||
string key, string value, bool remoteStore = true)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update))
|
||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Blocked));
|
||||
if (handle == null)
|
||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Validation));
|
||||
return handle.Update(key, value, remoteStore);
|
||||
}
|
||||
|
||||
internal static Task<PlayerKVFlushResult> FlushKVAsync(PlayerKVGameSessionHandle handle,
|
||||
CancellationToken waitToken = default)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
|
||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
||||
if (handle == null)
|
||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
||||
return handle.FlushAsync(waitToken);
|
||||
}
|
||||
|
||||
internal static Task<PlayerKVTicketResult> WaitForKVTicketAsync(PlayerKVGameSessionHandle handle,
|
||||
PlayerKVSyncTicket ticket, CancellationToken waitToken = default)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Cancelled,
|
||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
||||
if (handle == null)
|
||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
||||
return handle.WaitForTicketAsync(ticket, waitToken);
|
||||
}
|
||||
}
|
||||
11
Client/Assets/Scripts/Core/KVMgr.cs.meta
Normal file
11
Client/Assets/Scripts/Core/KVMgr.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e96f727fbe08473c89bb88150927d2be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -320,6 +320,8 @@ public class PlayFabMgr : MonoBehaviour
|
||||
GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString());
|
||||
}
|
||||
|
||||
internal void FlushBufferedData() => SyncData();
|
||||
|
||||
public async Task<bool> SyncDataImmidiate(string key, string data)
|
||||
{
|
||||
#if UNITY_EDITOR && !SERVICE_HOST
|
||||
@@ -353,51 +355,4 @@ public class PlayFabMgr : MonoBehaviour
|
||||
return await taskSource.Task;
|
||||
}
|
||||
|
||||
// BEGIN KV explicit fixed-handle access
|
||||
internal static PlayerKVKeyReadResult ReadKV(PlayerKVGameSessionHandle handle, string key)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.ReadLocal))
|
||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.LoadGuardActive, handle);
|
||||
if (handle == null || !KVSnapshot.ValidKey(key))
|
||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.InvalidInput, handle);
|
||||
var read = handle.ReadLocalSnapshot();
|
||||
if (!read.Success)
|
||||
return new PlayerKVKeyReadResult(read.Error == PlayerKVLocalReadError.StaleOrClosing
|
||||
? PlayerKVKeyReadError.StaleOrClosing : PlayerKVKeyReadError.NotReady, handle);
|
||||
var found = read.Data.TryGetValue(key, out var value);
|
||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.None, handle, found, value);
|
||||
}
|
||||
|
||||
internal static PlayerKVEditResult WriteKV(PlayerKVGameSessionHandle handle,
|
||||
string key, string value, bool remoteStore = true)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update))
|
||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Blocked));
|
||||
if (handle == null)
|
||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Validation));
|
||||
return handle.Update(key, value, remoteStore);
|
||||
}
|
||||
|
||||
internal static Task<PlayerKVFlushResult> FlushKVAsync(PlayerKVGameSessionHandle handle,
|
||||
CancellationToken waitToken = default)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
|
||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
||||
if (handle == null)
|
||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
||||
return handle.FlushAsync(waitToken);
|
||||
}
|
||||
|
||||
internal static Task<PlayerKVTicketResult> WaitForKVTicketAsync(PlayerKVGameSessionHandle handle,
|
||||
PlayerKVSyncTicket ticket, CancellationToken waitToken = default)
|
||||
{
|
||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Cancelled,
|
||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
||||
if (handle == null)
|
||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
||||
return handle.WaitForTicketAsync(ticket, waitToken);
|
||||
}
|
||||
// END KV explicit fixed-handle access
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user