using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; using game.PlayerDataStorage; if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || asap.core.GContext.Ready) throw new InvalidOperationException("Idle editor required."); var asm=typeof(PlayerKVGameSessionBridge).Assembly; var loop=asm.GetType("game.PlayerDataStorage.PlayerKVGameplayLoop",true); var flags=BindingFlags.NonPublic|BindingFlags.Public|BindingFlags.Static; var saved=loop.GetFields(flags).Where(f=>!f.IsInitOnly).ToDictionary(f=>f,f=>f.GetValue(null)); if((bool)loop.GetProperty("Enabled",flags).GetValue(null)) throw new InvalidOperationException("Real loop is active."); var runType=asm.GetType("game.PlayerDataStorage.PlayerKVSaveRouteEditorValidationHost+Run",true); var instanceFlags=BindingFlags.NonPublic|BindingFlags.Public|BindingFlags.Instance; int checks=0,faults=0; void Check(bool yes,string why) { checks++; if(!yes) throw new InvalidOperationException(why); } void Set(string name,object value) { var f=loop.GetField(name,flags) ?? loop.GetField("<"+name+">k__BackingField",flags); f.SetValue(null,value); } object Call(string name,params object[] args) { try { return loop.GetMethod(name,flags).Invoke(null,args); } catch(TargetInvocationException e) { throw e.InnerException; } } var folder=Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath,"../../CodexOutput/upserver/KVGameplayLoopTests/unity-synthetic",Guid.NewGuid().ToString("N"))); Directory.CreateDirectory(folder); var permitType=asm.GetType("game.PlayerDataStorage.PlayerKVLoopPermit",true); var permit=Activator.CreateInstance(permitType,true); permitType.GetProperty("PlayerId").SetValue(permit,"101"); permitType.GetProperty("AllowedKeys").SetValue(permit,new[]{"k","NewPlayerDataDic"}); object fixture=null; async Task Start() { fixture=Activator.CreateInstance(runType,true); var handler=(Func)Delegate.CreateDelegate(typeof(Func),fixture,runType.GetMethod("KV",instanceFlags)); var bridge=(PlayerKVGameSessionBridge)Activator.CreateInstance(typeof(PlayerKVGameSessionBridge),instanceFlags,null,new object[]{handler},null); runType.GetField("bridge",instanceFlags).SetValue(fixture,bridge); var handle=await (Task)runType.GetMethod("Open",instanceFlags).Invoke(fixture,new object[]{"101"}); Set("Enabled",true); Set("Ready",false); Set("failed",0); Set("Error",null); Set("handle",null);Set("collecting",null); Set("Permit",permit);Set("RunPath",folder);Set("mainThread",Thread.CurrentThread.ManagedThreadId);Set("mainContext",SynchronizationContext.Current); Set("onFault",new Action(_=>faults++));Set("onClose",null); Call("Publish",handle); return handle; } async Task CloseFixture() { if(fixture!=null) await (Task)runType.GetMethod("Close",instanceFlags).Invoke(fixture,null); fixture=null; } try { await Start(); Check((string)Call("Read","missing")==null,"valid missing key remains null"); var batch=Call("BeginEquip"); var nested=(PlayerKVEditResult)Call("Write","k","nested",true); var primary=(PlayerKVEditResult)Call("Write","NewPlayerDataDic","whole-save",true); ((IDisposable)batch).Dispose(); await (Task)Call("ConfirmEquipAsync",batch,primary); Check(nested.Ticket!=null && primary.Ticket!=null,"both exact tickets exist"); var remote=(Dictionary>)runType.GetField("remote",instanceFlags).GetValue(fixture); Check(remote["101"]["k"]=="nested" && remote["101"]["NewPlayerDataDic"]=="whole-save","nested and primary both remotely confirmed"); Check(faults==0,"successful equipment remains ready"); await CloseFixture(); await Start(); batch=Call("BeginEquip"); var superseded=(PlayerKVEditResult)Call("Write","k","first",true); Call("Write","k","second",true); primary=(PlayerKVEditResult)Call("Write","NewPlayerDataDic","whole-save",true); ((IDisposable)batch).Dispose(); bool rejected=false;try { await (Task)Call("ConfirmEquipAsync",batch,primary); } catch(InvalidOperationException e) { rejected=e.Message.Contains("Superseded"); } Check(rejected && faults==1,"nested Superseded stops the entire equip chain"); Check(!(bool)loop.GetProperty("Ready",flags).GetValue(null),"fault synchronously revokes facade"); await CloseFixture(); await Start(); var before=(int)runType.GetField("writes",instanceFlags).GetValue(fixture); rejected=false;try { Call("Write","unbacked-key","x",true); } catch(InvalidOperationException e) { rejected=e.Message.Contains("permission"); } Check(rejected && (int)runType.GetField("writes",instanceFlags).GetValue(fixture)==before,"unauthorized key rejected before network"); await CloseFixture(); var closed=await Start(); await CloseFixture(); rejected=false;try { Call("Read","k"); } catch(InvalidOperationException) { rejected=true; } Check(rejected,"failed stale read is not missing key"); Check(faults==3,"each separate synthetic run reports one fault"); return new{success=true,assertions=checks,formalRequests=0,synthetic=true,restored=true,folder}; } finally { await CloseFixture(); // Do not restore over an undrained failure. foreach(var pair in saved) pair.Key.SetValue(null,pair.Value); }