Files
ft/Client/Assets/Scripts/Services/PlayerData/PlayerKVControlledLoadCapability.cs
Liubing\LB 6813da149a feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
2026-09-30 19:19:05 +08:00

132 lines
7.6 KiB
C#

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using game.Account;
namespace game.PlayerDataStorage
{
// The lease-only capability remains source-compatible with the independent 4B1 regression host.
// This separate capability supplies every dependency required by a complete controlled load.
internal sealed class PlayerKVGameLoadCapability
{
internal PlayerKVIdentitySnapshot Identity { get; }
internal PlayerKVSchemaSnapshot Snapshot { get; }
internal Uri Api { get; }
internal string CacheRoot { get; }
internal int LoginObserveMs { get; }
internal int QuitObserveMs { get; }
internal bool RequireExistingArchive { get; }
internal Func<int,CancellationToken,Task> Delay { get; }
internal PlayerKVControlledLoadCapability LeaseCapability { get; private set; }
readonly PlayerKVControlledLoadCapability auth,kv,lease;
readonly object gate=new object();
CookieContainer cookies;
HttpMessageHandler pendingKV,pendingLease;
Exception cleanupFailure;
readonly System.Collections.Generic.List<HttpMessageHandler> failedHandlers=new System.Collections.Generic.List<HttpMessageHandler>();
bool claimed;
private PlayerKVGameLoadCapability(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,Uri api,string cacheRoot,
int loginMs,int quitMs,Func<int,CancellationToken,Task> delay,
PlayerKVControlledLoadCapability auth,PlayerKVControlledLoadCapability kv,PlayerKVControlledLoadCapability lease,
bool requireExistingArchive=false)
{
Identity=identity ?? throw new ArgumentNullException(nameof(identity)); Snapshot=snapshot ?? throw new ArgumentNullException(nameof(snapshot));
if(api==null || !api.IsAbsoluteUri || (api.Scheme!="http" && api.Scheme!="https") || !string.IsNullOrEmpty(api.UserInfo) ||
!string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment) || string.IsNullOrEmpty(cacheRoot) ||
loginMs<1000 || loginMs>15000 || quitMs<1000 || quitMs>15000) throw new ArgumentException("Controlled load configuration invalid.");
Api=new Uri(api.AbsoluteUri.TrimEnd('/')+"/"); CacheRoot=cacheRoot;
LoginObserveMs=loginMs; QuitObserveMs=quitMs; Delay=delay ?? Task.Delay; this.auth=auth; this.kv=kv; this.lease=lease;
RequireExistingArchive=requireExistingArchive;
}
internal static PlayerKVGameLoadCapability CreateForGame(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,
Uri api,string cacheRoot,Func<CookieContainer,HttpMessageHandler> auth,Func<CookieContainer,HttpMessageHandler> kv,
Func<CookieContainer,HttpMessageHandler> lease)
{
if(api==null || api.AbsoluteUri!="http://s101.local:18080/")
throw new ArgumentException("Formal KV endpoint mismatch.",nameof(api));
var capability=new PlayerKVGameLoadCapability(identity,snapshot,api,cacheRoot,10000,5000,null,
PlayerKVControlledLoadCapability.CreateForGame(auth),PlayerKVControlledLoadCapability.CreateForGame(kv),
PlayerKVControlledLoadCapability.CreateForGame(lease),true);
capability.LeaseCapability=PlayerKVControlledLoadCapability.CreateForGame(capability.TakeLease);
return capability;
}
#if UNITY_EDITOR
internal static PlayerKVGameLoadCapability CreateForValidation(PlayerKVIdentitySnapshot identity,PlayerKVSchemaSnapshot snapshot,
Uri api,string cacheRoot,Func<CookieContainer,HttpMessageHandler> auth,Func<CookieContainer,HttpMessageHandler> kv,
Func<CookieContainer,HttpMessageHandler> lease,int loginMs=10000,int quitMs=5000,Func<int,CancellationToken,Task> delay=null)
{
var capability=new PlayerKVGameLoadCapability(identity,snapshot,api,cacheRoot,loginMs,quitMs,delay,
PlayerKVControlledLoadCapability.CreateForValidation(auth),PlayerKVControlledLoadCapability.CreateForValidation(kv),
PlayerKVControlledLoadCapability.CreateForValidation(lease));
capability.LeaseCapability=PlayerKVControlledLoadCapability.CreateForValidation(capability.TakeLease);
return capability;
}
#endif
internal PlayerKVLoginContext CreateContext(string id,long epoch,Func<long,bool> current)
{
lock(gate) { if(claimed) throw new InvalidOperationException("Controlled capability already used."); claimed=true; }
return new PlayerKVLoginContext(id,CacheRoot,epoch,current,Api,
()=>new S101AccountSession(Api,CreateAuthentication),()=>new PlayerKVGameSessionBridge(TakeKV));
}
HttpMessageHandler CreateAuthentication(CookieContainer jar)
{
lock(gate)
{
if(cookies!=null) throw new InvalidOperationException("Repeated authentication factory.");
cookies=jar; HttpMessageHandler authentication=null;
try
{
// Validate all three returned handler graphs before the first authentication send.
authentication=auth.CreateHandler(jar); pendingKV=kv.CreateHandler(jar); pendingLease=lease.CreateHandler(jar);
if(ShareHandlerGraph(authentication,pendingKV,pendingLease))
throw new InvalidOperationException("Distinct transport ownership required.");
return authentication;
}
catch
{
try { authentication?.Dispose(); } catch(Exception ex) { cleanupFailure=ex; failedHandlers.Add(authentication); }
try { CloseUnused(); } catch { }
throw;
}
}
}
static bool ShareHandlerGraph(params HttpMessageHandler[] roots)
{
var seen=new System.Collections.Generic.List<HttpMessageHandler>();
foreach(var root in roots)
for(var node=root;node!=null;node=(node as DelegatingHandler)?.InnerHandler)
{
if(seen.Exists(previous=>ReferenceEquals(previous,node))) return true;
seen.Add(node);
}
return false;
}
HttpMessageHandler TakeKV(CookieContainer jar)
{
lock(gate)
{ if(!ReferenceEquals(jar,cookies) || pendingKV==null) throw new InvalidOperationException("KV transport ownership invalid.");
var result=pendingKV; pendingKV=null; return result; }
}
HttpMessageHandler TakeLease(CookieContainer jar)
{
lock(gate)
{ if(!ReferenceEquals(jar,cookies) || pendingLease==null) throw new InvalidOperationException("Lease transport ownership invalid.");
var result=pendingLease; pendingLease=null; return result; }
}
internal void CloseUnused()
{
lock(gate)
{
// Attempt every owned cleanup once; retain a failure even if a later Dispose succeeds.
var ownedLease=pendingLease; pendingLease=null;
var ownedKV=pendingKV; pendingKV=null;
try { ownedLease?.Dispose(); } catch(Exception ex) { cleanupFailure=cleanupFailure ?? ex; failedHandlers.Add(ownedLease); }
try { ownedKV?.Dispose(); } catch(Exception ex) { cleanupFailure=cleanupFailure ?? ex; failedHandlers.Add(ownedKV); }
if(cleanupFailure!=null) throw new InvalidOperationException("Controlled transport cleanup failed.",cleanupFailure);
}
}
}
}