feat(upserver): 保存M2客户端KV迁移至本人统计写检查点

保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
Liubing\LB
2026-09-30 19:19:05 +08:00
parent 1c0cdf7a99
commit 6813da149a
511 changed files with 151858 additions and 2376 deletions

View File

@@ -9,7 +9,7 @@ using Newtonsoft.Json;
using PlayFab.ClientModels;
using tysdk;
static class Probe
static partial class Probe
{
static int assertions,groups;
static void Check(bool condition,string label){assertions++;if(!condition)throw new Exception(label);}
@@ -57,12 +57,33 @@ static class Probe
}
}
Pass("public-backend-rejection-before-AGG-shortcut");
SubBoostrap.RuntimeSelection=PlayerKVRuntimeSelection.Capture("kv",PlayerKVRuntimeSelection.FormalApiUrl,false);
var formalConfig=Configuration(); formalConfig.Set("SaveBackend","kv");
foreach(var disallowed in new[]{"100","103","-101","x101"})
{
var formalUser=new KVUserService{CustomId=disallowed};
Effects.Clear(); PlayFab.PlayFabClientAPI.Logged=true;
Check(await formalUser.Login(null)==ELoginStep.Api && formalUser.KVLastError==PlayerKVGameLoadError.Input,
"formal cohort rejected before any account request");
Check(!formalUser.IsLogin && !formalUser.IsAuthorized && Effects.Total==0,
"formal rejection does not publish PF identity or cause side effects");
formalUser.Logout();
Check(Effects.SDK==0,"formal logout does not clear PF credentials");
}
var quittingUser=new KVUserService{CustomId="101"};
var oldLifetime=quittingUser.LoginLifetime; Effects.Clear();
UnityEngine.Application.QuitForTest();
Check(oldLifetime.IsCancellationRequested && Effects.SDK==0,
"application quitting revokes formal login lifetime without PF credential calls");
SubBoostrap.RuntimeSelection=null;
Pass("formal-cohort-rejection-and-PF-credential-isolation");
var pf=new PlayFabMgr();Private(pf,"Awake");
var data=new Dictionary<string,UserDataRecord>{{"MMTest",new(){Value="MMFailParam#4",LastUpdated=DateTime.UtcNow}},{"TapJoy_Diamond",new(){Value="7",LastUpdated=DateTime.UtcNow}}};
pf.CompareDatas(data,"synthetic");pf.UpdateUserDataValue("local","value",false);
Check(pf.GetLocalData("local")=="value","default PF local write unchanged");
Check(await pf.SyncDataImmidiate("remote","value"),"default PF immediate result unchanged");
Pass("default-PF-storage-passthrough");
await MergedPFStore(pf);
foreach(bool agg in new[]{false,true})
{
var config=Configuration();PlayFabUserService user=agg?new AggUserService():new PlayFabUserService();user.config=config;user.CustomId="101";
@@ -122,6 +143,9 @@ static class Probe
Check(Effects.Total==0 && fresh.IsUnstartedForKVLoad && guard.Violations==12,"all PF entrances reject before buffer/Prefs/SDK");
}
Pass("all-real-PF-guard-entrances");await Controlled();
await FormalTransport();
await MergedUser();
await StatisticsWriteTests();
}
sealed class Chat:IChatService {public void WaitingForLogin(){} }
sealed class Setting:ISettingService {public string Lang=>"en";}
@@ -158,6 +182,87 @@ static class Probe
Check(unauthorizedEvents==1 && auth==3 && Effects.Total==0 && user.KVIntents==null,"late 401 never reauthenticates or publishes another unauthorized event");
Pass("real-Agg-User-CustomServer-coordinator-KV-chain");
}
static async Task FormalTransport()
{
var api=new Uri(PlayerKVRuntimeSelection.FormalApiUrl);
var config=new Config();config.Set("EventApiURL",api.AbsoluteUri);
int auth=0,kvGet=0,business=0,unauthorized=0;
var ownerThread=Environment.CurrentManagedThreadId;
CookieContainer jar=null;
var context=new PlayerKVLoginContext("101",Path.Combine(Path.GetTempPath(),"ft-cp2b-t-"+Guid.NewGuid().ToString("N")),31,
_=>true,api,
()=>new S101AccountSession(api,cookies=>new Handler(r=>
{
auth++;jar=cookies;cookies.Add(api,new Cookie("sid","synthetic"));
return Json(new{state=0,internalId=17,newCreate=false,createTime="2026-01-01T00:00:00Z",svrTime="2026-01-01T00:00:00Z"});
})),
()=>new PlayerKVGameSessionBridge(cookies=>new Handler(r=>
{kvGet++;Check(ReferenceEquals(cookies,jar),"KV shares account jar");return Json(new{data=new Dictionary<string,string>()});})));
Check((await context.PrepareAsync()).Prepared,"formal synthetic account prepared");
var late=new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
Check(context.TryAcquireAccountLease(PlayerKVControlledLoadCapability.CreateForGame(cookies=>new Handler(r=>
{
Check(ReferenceEquals(cookies,jar),"ordinary send shares account jar");business++;
Check(Environment.CurrentManagedThreadId==ownerThread,"ordinary transport runs on owner thread");
return r.RequestUri.AbsolutePath switch
{
"/friend/late"=>late.Task,
"/friend/unauthorized"=>Json(new{},HttpStatusCode.Unauthorized),
"/mail/missing"=>Json(new{},HttpStatusCode.NotFound),
_=>Json(new{ok=true})
};
})),out var lease),"formal lease acquired");
var server=new CustomServerMgr(config,_=>new Handler(r=>throw new Exception("PF transport forbidden")));
server.KVUnauthorized+=_=>unauthorized++;
server.BindKVForGame(lease);
try { await server.CustomServerPost("mail/fetch","{}");throw new Exception("pre-ready send escaped"); }
catch(InvalidOperationException) { Check(business==0,"pre-ready zero send"); }
server.EnableKVBusiness(lease.Epoch);
Check(ReferenceEquals(server.Cookie,jar),"RT sees same account cookie only after readiness");
Check((await server.CustomServerPost("mail/fetch","{}"))!=null && business==1,"business POST uses lease");
Check((await server.GetStringAsync("anti-cheat/GetConfigRequest"))!=null && business==2,"business GET uses lease");
Check(await Task.Run(()=>server.GetStringAsync("friend/background"))!=null && business==3,
"background caller dispatched to original service thread");
Check(await server.GetStringAsync("mail/missing")==null && business==4 && unauthorized==0,
"non-200 response returns empty without revoking identity");
var old=server.GetStringAsync("friend/late");
var version=server.SessionVersion;
Check(await server.GetStringAsync("friend/unauthorized")==null && unauthorized==1 && server.SessionVersion>version,
"current 401 disables transport and advances version");
Check(business==6 && auth==1 && kvGet==1,"one authentication, one KV read, no PF transport");
try { server.EnableKVBusiness(lease.Epoch);throw new Exception("revoked transport reopened"); }
catch(InvalidOperationException) { Check(true,"single publication cannot reopen after 401"); }
try { _=server.Cookie;throw new Exception("revoked cookie escaped"); }
catch(NullReferenceException) { Check(true,"revoked cookie unavailable"); }
var close=server.ClearKVForGame();Check(!close.IsCompleted,"inflight request drains before closing");
late.SetResult(new HttpResponseMessage(HttpStatusCode.OK){Content=new StringContent("old-session-body")});
Check(await old==null,"old 200 body withheld after unauthorized revocation");
await close;await context.CloseAsync();
CookieContainer nextJar=null;
var nextContext=new PlayerKVLoginContext("101",Path.Combine(Path.GetTempPath(),"ft-cp2b-t-"+Guid.NewGuid().ToString("N")),32,
_=>true,api,
()=>new S101AccountSession(api,cookies=>new Handler(r=>
{
auth++;nextJar=cookies;cookies.Add(api,new Cookie("sid","next-synthetic"));
return Json(new{state=0,internalId=17,newCreate=false,createTime="2026-01-01T00:00:00Z",svrTime="2026-01-01T00:00:00Z"});
})),
()=>new PlayerKVGameSessionBridge(cookies=>new Handler(r=>Json(new{data=new Dictionary<string,string>()}))));
Check((await nextContext.PrepareAsync()).Prepared,"same account new epoch prepared");
Check(nextContext.TryAcquireAccountLease(PlayerKVControlledLoadCapability.CreateForGame(cookies=>new Handler(r=>
{
Check(ReferenceEquals(cookies,nextJar) && !ReferenceEquals(cookies,jar),"new epoch owns new cookie jar");
return Json(new{next=true});
})),out var nextLease),"new epoch lease acquired");
server.BindKVForGame(nextLease);
Check(!ReferenceEquals(lease,nextLease),"new epoch has distinct lease");
try { _=server.Cookie;throw new Exception("new epoch cookie published before readiness"); }
catch(NullReferenceException) { Check(true,"new epoch cookie withheld before readiness"); }
server.EnableKVBusiness(nextLease.Epoch);
Check(ReferenceEquals(server.Cookie,nextJar) && await server.GetStringAsync("friend/next")!=null,
"same account new epoch uses only its current lease");
await server.ClearKVForGame();await nextContext.CloseAsync();
Pass("formal-KV-business-transport-and-401-boundary");
}
}