保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
269 lines
22 KiB
C#
269 lines
22 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Globalization;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using game;
|
|
using game.Account;
|
|
using game.PlayerDataStorage;
|
|
using Newtonsoft.Json;
|
|
using PlayFab.ClientModels;
|
|
using tysdk;
|
|
|
|
static partial class Probe
|
|
{
|
|
static int assertions,groups;
|
|
static void Check(bool condition,string label){assertions++;if(!condition)throw new Exception(label);}
|
|
sealed class Loop:SynchronizationContext
|
|
{ internal BlockingCollection<(SendOrPostCallback,object)> Queue=new();public override void Post(SendOrPostCallback c,object s)=>Queue.Add((c,s)); }
|
|
static int Main() { try { RunHost(); return 0; } catch(Exception ex) { Console.WriteLine(ex); return 1; } }
|
|
static void RunHost()
|
|
{
|
|
var loop=new Loop();SynchronizationContext.SetSynchronizationContext(loop);
|
|
var task=Run();_=task.ContinueWith(_=>loop.Post(_=>{},null),TaskScheduler.Default);
|
|
while(!task.IsCompleted){var(c,s)=loop.Queue.Take();c(s);}task.GetAwaiter().GetResult();
|
|
#if UNITY_ANDROID
|
|
const string platform="ANDROID+AGG";
|
|
#else
|
|
const string platform="NON_ANDROID";
|
|
#endif
|
|
Console.WriteLine($"RESULT service-host {platform} groups={groups} assertions={assertions} formalRequests=0");
|
|
}
|
|
sealed class Handler:HttpMessageHandler
|
|
{
|
|
readonly Func<HttpRequestMessage,Task<HttpResponseMessage>> send;
|
|
internal Handler(Func<HttpRequestMessage,Task<HttpResponseMessage>> send)=>this.send=send;
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r,CancellationToken t)=>send(r);
|
|
}
|
|
static Task<HttpResponseMessage> Json(object value,HttpStatusCode status=HttpStatusCode.OK)=>Task.FromResult(new HttpResponseMessage(status){Content=new StringContent(JsonConvert.SerializeObject(value))});
|
|
static readonly Uri Api=new("http://synthetic.invalid/");
|
|
static Config Configuration(){var c=new Config();c.Set("EventApiURL",Api.AbsoluteUri);c.Set("DEBUG","1");return c;}
|
|
static readonly string[] Core={"3","4","4","5","6","7","101","2026-09-23T00:00:00Z","-1","8","9","9","1","0","10","11","12","13","14"};
|
|
static Dictionary<string,string> Raw()=>new(){["NewPlayerDataDic"]=JsonConvert.SerializeObject(new{newData=string.Join(",",Core)}),["TapJoy_Diamond"]="9"};
|
|
static PlayerKVSchemaSnapshot Snapshot()=>new("stub-v1",new[]{101},new[]{1,2},new Dictionary<string,int>{{"None",0}},CultureInfo.InvariantCulture,new DateTime(2026,9,23,0,0,0,DateTimeKind.Utc),17);
|
|
static PlayerKVIdentitySnapshot Identity()=>new("FF",false,PlayerKVIdentityChannel.hwGuest,Array.Empty<PlayerKVIdentityChannel>());
|
|
static object Private(object target,string method,params object[] args)=>target.GetType().GetMethod(method,BindingFlags.Instance|BindingFlags.NonPublic,null,args.Select(a=>a.GetType()).ToArray(),null).Invoke(target,args);
|
|
static void Pass(string name){groups++;Console.WriteLine("PASS "+name);}
|
|
static async Task Run()
|
|
{
|
|
foreach(bool agg in new[]{false,true})
|
|
{
|
|
var config=Configuration();PlayFabUserService user=agg?new AggUserService():new PlayFabUserService();user.config=config;user.CustomId="101";
|
|
foreach(var backend in new[]{"kv","KV","unknown","",null})
|
|
{
|
|
config.Set("SaveBackend",backend);Effects.Clear();
|
|
Check(await user.Login(null)==ELoginStep.Api,"public backend gate");
|
|
Check(user.KVLastError==(backend=="kv"?PlayerKVGameLoadError.BackendNotEnabled:PlayerKVGameLoadError.BackendUnsupported),"typed rejection");
|
|
Check(Effects.Total==0,"before all SDK/event access");
|
|
}
|
|
}
|
|
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";
|
|
var server=new CustomServerMgr(config,_=>new Handler(r=>Json(new{})));user.customServerMgr=server;
|
|
using var session=new S101AccountSession(Api,jar=>new Handler(r=>{jar.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"});}));
|
|
Check((await session.LoginAsync("101")).Success,"PF synthetic account");
|
|
typeof(PlayFabUserService).GetField("apiSession",BindingFlags.NonPublic|BindingFlags.Instance).SetValue(user,session);
|
|
typeof(PlayFabUserService).GetProperty("InternalId").SetValue(user,(uint)17);PlayFab.PlayFabClientAPI.Logged=true;
|
|
int progress=0;Check(await user.Login(v=>progress=v)==ELoginStep.Done && progress==(agg?3:2),"default PF authorized shortcut");
|
|
config.Set("SaveBackend","kv");Effects.Clear();Check(await user.Login(null)==ELoginStep.Api && Effects.Total==0,"authorized cannot bypass backend gate");
|
|
typeof(PlayFabUserService).GetField("apiSession",BindingFlags.NonPublic|BindingFlags.Instance).SetValue(user,null);
|
|
}
|
|
Pass("default-PF-and-AGG-shortcuts");
|
|
GContext.container.Values[typeof(ISettingService)]=new Setting();GContext.container.Values[typeof(IChatService)]=new Chat();
|
|
GContext.container.Values[typeof(PlayerData)]=new PlayerData();GContext.container.Values[typeof(MMTService)]=new MMTService();
|
|
asap.playfab.async.PlayFabClientAsyncAPI.Result=new LoginResult{PlayFabId="FF",NewlyCreated=false,
|
|
EntityToken=new EntityTokenResponse{Entity=new EntityKey()},InfoResultPayload=new GetPlayerCombinedInfoResultPayload{UserData=data,AccountInfo=new UserAccountInfo{Created=DateTime.UtcNow},PlayerProfile=new PlayerProfileModel{DisplayName="Synthetic",AvatarUrl="synthetic",Locations=new(){new LocationModel()}}}};
|
|
// Invalid scheme ensures legacy API stage fails before HttpClient; no real endpoint is contacted.
|
|
var normalConfig=Configuration();normalConfig.Set("EventApiURL","ftp://synthetic.invalid/");
|
|
var normal=new PlayFabUserService{config=normalConfig,CustomId="101",customServerMgr=new CustomServerMgr(normalConfig,_=>new Handler(r=>throw new Exception("no HTTP")))};
|
|
PlayFab.PlayFabClientAPI.Logged=false;Check(await normal.Login(null)==ELoginStep.Api,"normal PF reaches API boundary");
|
|
Check(((PlayerData)GContext.container.Values[typeof(PlayerData)]).Loads==1 && normal.MMTestForKVValidation==4,"real PF init after adjusted time");
|
|
var hotConfig=Configuration();hotConfig.Set("EventApiURL","ftp://synthetic.invalid/");
|
|
var hot=new AggUserService{config=hotConfig,customServerMgr=new CustomServerMgr(hotConfig,_=>new Handler(r=>throw new Exception("no HTTP")))};
|
|
var sdk=new TaskCompletionSource<LoginInfo>();TYSdkFacade.IsLoggedIn=false;TYSdkFacade.LoginOverride=()=>sdk.Task;PlayFab.PlayFabClientAPI.Logged=false;
|
|
var pending=hot.Login(null);hotConfig.Set("SaveBackend","kv");sdk.SetResult(new(){isSuccess=true,userId=101});
|
|
Check(await pending==ELoginStep.Api && hot.KVLastError==PlayerKVGameLoadError.None,"captured PF branch continues to API failure");
|
|
Check(((PlayerData)GContext.container.Values[typeof(PlayerData)]).Loads==2,"AGG real PF init window");TYSdkFacade.LoginOverride=null;
|
|
Pass("PF-original-init-order-and-backend-capture");
|
|
UnityEngine.GameObject.Found=new TapjoyUnity.Internal.TapjoyComponent();Effects.Clear();
|
|
typeof(PlayFabUserService).GetMethod("InitTapJoy",BindingFlags.Static|BindingFlags.NonPublic).Invoke(null,null);
|
|
#if UNITY_ANDROID
|
|
Check(Effects.Helpers==2,"PF Android creates and initializes helper");
|
|
#else
|
|
Check(Effects.Helpers==0,"PF non-Android no helper");
|
|
#endif
|
|
Pass("PF-original-TapJoy-platform-branch");
|
|
UniRx.Stream<EvtAPISvrUnauthorized>.Handlers=null;
|
|
var unauthorizedUser=new PlayFabUserService{config=normalConfig,CustomId="101",customServerMgr=new CustomServerMgr(normalConfig,_=>new Handler(r=>throw new Exception("no HTTP")))};
|
|
typeof(PlayFabUserService).GetProperty("InternalId").SetValue(unauthorizedUser,(uint)17);PlayFab.PlayFabClientAPI.Logged=true;
|
|
int version=unauthorizedUser.customServerMgr.SessionVersion;
|
|
GContext.Publish(new EvtAPISvrUnauthorized{SessionVersion=version-1});Check(unauthorizedUser.InternalId==17,"PF stale 401 ignored");
|
|
GContext.Publish(new EvtAPISvrUnauthorized{SessionVersion=version});
|
|
Check(unauthorizedUser.InternalId==0 && unauthorizedUser.customServerMgr.SessionVersion>version,"PF current 401 retains old reauthentication branch, invalid API prevents HTTP");
|
|
UniRx.Stream<EvtAPISvrUnauthorized>.Handlers=null;
|
|
Pass("PF-current-and-stale-401-branches");
|
|
Private(pf,"OnDestroy"); typeof(PlayFabMgr).GetField("_instance",BindingFlags.Static|BindingFlags.NonPublic).SetValue(null,null);
|
|
var fresh=new PlayFabMgr();Check(fresh.IsUnstartedForKVLoad,"fresh PF component");
|
|
Check(PlayerKVGameLoadGuard.TryEnter(out var guard),"guard entered");
|
|
using(guard)
|
|
{
|
|
Effects.Clear();
|
|
foreach(Action attempt in new Action[]{()=>fresh.CompareDatas(data,"x"),()=>fresh.GetLocalData("x"),()=>fresh.UpdateUserDataValue("x","y"),()=>fresh.UpdateUserDataValue("x","y",false)})
|
|
{try{attempt();throw new Exception("guard missed");}catch(PlayerKVLoadSideEffectException){assertions++;}}
|
|
Check(!await fresh.SyncDataImmidiate("x","y"),"immediate rejected");
|
|
Private(fresh,"SyncData");Private(fresh,"Awake");Private(fresh,"OnApplicationPause",true);Private(fresh,"OnApplicationPause",false);Private(fresh,"OnApplicationQuit");Private(fresh,"OnDestroy");Private(fresh,"UpdateUserDataValue",CancellationToken.None);
|
|
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";}
|
|
static async Task Controlled()
|
|
{
|
|
int auth=0,get=0,borrow=0,unauthorizedEvents=0;Dictionary<string,string> raw=Raw();
|
|
Task<HttpResponseMessage> delayedBorrow=null;
|
|
var config=Configuration();var user=new AggUserService(PlayerKVControlledLoadCapability.CreateForValidation(_=>new Handler(r=>throw new Exception()))){config=config,CustomId="101"};
|
|
var server=new CustomServerMgr(config,_=>new Handler(r=>throw new Exception("PF transport forbidden")));
|
|
server.KVUnauthorized+=_=>unauthorizedEvents++;
|
|
var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id,()=>{if(id==31)user.ResetForKVLoad();},decoded=>{if(id==31)user.ReplaceFromKVDecoded(decoded.Module<PlayerKVLoginState>(31),decoded.Epoch,decoded.Raw);})).ToArray();
|
|
user.ConfigureKVForValidation(new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>user.KVIntents?.Discard()),new PlayFabMgr(),server,()=>true);
|
|
PlayerKVGameLoadCapability Capability()=>PlayerKVGameLoadCapability.CreateForValidation(Identity(),Snapshot(),Api,Path.Combine(Path.GetTempPath(),"ft-kv-service-probe",Guid.NewGuid().ToString("N")),
|
|
jar=>new Handler(r=>{auth++;jar.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"});}),
|
|
jar=>new Handler(r=>{get++;return Json(new{data=raw});}),jar=>new Handler(r=>{borrow++;return delayedBorrow ?? Json(new{},HttpStatusCode.Unauthorized);}));
|
|
Effects.Clear();
|
|
foreach(var invalid in new[]{"0","999","15001","bad","1.5"})
|
|
{config.Set("KVLoginCloseObserveMs",invalid);Check((await user.LoadKVForValidationAsync(Capability())).Error==PlayerKVGameLoadError.Input,"invalid option rejected");Check(auth==0 && user.KVReceiverCountForValidation==0,"invalid no send/subscription leak");}
|
|
config.Values.Remove("KVLoginCloseObserveMs");
|
|
var result=await user.LoadKVForValidationAsync(Capability());
|
|
Check(result.State==PlayerKVGameLoadState.LoadedForValidation && user.KVLoadedForValidation,"real service controlled success: "+result.Error);
|
|
Check(auth==1 && get==1 && user.KVNotificationCount==1 && user.KVReceiverCountForValidation==0,"one auth/Get/windowed receiver");
|
|
Check(!user.IsLogin && !user.IsAuthorized && user.InternalId==0 && user.KVTapJoyBalance==9 && Effects.Total==0,"zero SDK/Prefs/reward/UI/helpers/events in KV");
|
|
var intents=user.KVIntents;await server.SendKVForValidationAsync("__kv_validation/unauthorized");await user.CloseKVForValidationAsync();
|
|
Check(!user.KVLoadedForValidation && user.KVIntents==null && intents.Discarded && borrow==1 && auth==1,"401 revokes without reauth");
|
|
raw.Clear();var failed=await user.LoadKVForValidationAsync(Capability());
|
|
Check(failed.Error==PlayerKVGameLoadError.Schema && user.KVReceiverCountForValidation==0 && user.KVNotificationCount==1,"failure removes receiver without notification");
|
|
Check(Effects.Total==0,"all failure paths zero effects");
|
|
raw=Raw();Check((await user.LoadKVForValidationAsync(Capability())).State==PlayerKVGameLoadState.LoadedForValidation,"new epoch after successful cleanup");
|
|
var late=new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);delayedBorrow=late.Task;
|
|
var oldRequest=server.SendKVForValidationAsync("__kv_validation/unauthorized");user.Logout();Check(!user.KVLoadedForValidation,"logout synchronously revoked");
|
|
late.SetResult(new HttpResponseMessage(HttpStatusCode.Unauthorized){Content=new StringContent("{}")});
|
|
Check(!(await oldRequest).IsCurrent,"late 401 body discarded");await user.CloseKVForValidationAsync();
|
|
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");
|
|
}
|
|
}
|
|
|
|
|