feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user