using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using HotUpdate.Gameplay; using HotUpdate.Net; using HotUpdate.UI; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; /// Real UI/HTTP on an isolated database. Supports restarting the test server between two entries. public static class VerifyBattleRuntime { const string Endpoint="http://127.0.0.1:18083"; static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../..")); static PrototypeFlow Flow=>UnityEngine.Object.FindObjectOfType(); static T Get(string name)=>(T)typeof(PrototypeFlow).GetField(name,BindingFlags.Instance|BindingFlags.NonPublic).GetValue(Flow); static void Set(string name,object value)=>typeof(PrototypeFlow).GetField(name,BindingFlags.Instance|BindingFlags.NonPublic).SetValue(Flow,value); static object Call(string name)=>typeof(PrototypeFlow).GetMethod(name,BindingFlags.Instance|BindingFlags.NonPublic).Invoke(Flow,null); static void Check(bool v,string message){if(!v)throw new Exception(message);} static async Task Until(Func condition,int seconds=35){var end=DateTime.UtcNow.AddSeconds(seconds);while(!condition()){Check(DateTime.UtcNow().Single(b=>b.name=="Btn_"+name&&b.gameObject.activeInHierarchy);Check(button.interactable,"Disabled "+name);Canvas.ForceUpdateCanvases();var rect=button.GetComponent(); var pointer=new PointerEventData(EventSystem.current){position=RectTransformUtility.WorldToScreenPoint(null,rect.TransformPoint(rect.rect.center)),button=PointerEventData.InputButton.Left};var hits=new List();EventSystem.current.RaycastAll(pointer,hits); Check(hits.Count>0&&ExecuteEvents.GetEventHandler(hits[0].gameObject)==button.gameObject,"Blocked "+name);ExecuteEvents.Execute(button.gameObject,pointer,ExecuteEvents.pointerClickHandler); } static async Task ClickReady(string name){for(int n=0;;n++){try{Click(name);return;}catch(Exception)when(n<40){await Task.Delay(50);}}} static async Task Capture() { var type=AppDomain.CurrentDomain.GetAssemblies().Select(a=>a.GetType("Unity.Pipeline.Editor.Commands.Capture.CaptureCommands")).First(t=>t!=null); type.GetMethod("CaptureGameView",BindingFlags.Public|BindingFlags.Static).Invoke(null,new object[]{1080,1920,null,"Screenshots/battle_skills_network.png",false,0,"screen"});await Task.Delay(150); } public static string Assets() { var arena=AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab");var player=arena.GetComponentInChildren(true);Check(player!=null&&player.EffectKinds==15&&player.ProfileCount==10,"Missing formal skill bank");var so=new SerializedObject(player);int count=0; for(int i=0;i<15;i++){var list=so.FindProperty("_effects").GetArrayElementAtIndex(i).FindPropertyRelative("instances");count+=list.arraySize;for(int n=0;n(); foreach(string file in files) { var data=JObject.Parse(File.ReadAllText(file));var manifest=JsonUtility.FromJson(data["manifest"].ToString());var events=data["events"].ToObject();Check(CombatTraceValidation.Events(manifest,events),"Invalid exported trace"); var snapshot=JsonUtility.FromJson(data["save"].ToString());var config=JsonUtility.FromJson(data["config"].ToString());var stage=JsonUtility.FromJson(data["stage"].ToString());var game=new PrototypeGame(PrototypeSave.BattleSnapshot(snapshot.level,snapshot.stages,snapshot.formation));var battle=new PrototypeBattle("domain-authoritative-"+stage.id,game,config,stage.id); battle.LoadTrace(manifest,events);for(int i=0;i<2401&&!battle.Finished;i++){battle.Tick();while(battle.TryDequeueCue(out var _)){}while(battle.TryDequeueSkillCue(out var _)){};} Check(battle.TraceFinalStateMatches&&battle.TraceEventsConsumed==events.Length,"Final read model mismatch "+file); foreach(var stat in data["manifest"]["stats"]??new JArray()){var h=battle.Heroes.Single(x=>x.Pet==(int)stat["pet"]);Check(Mathf.Abs(h.DamageDealt-(float)stat["damage"])<.01f&&Mathf.Abs(h.Healing-(float)stat["healing"])<.01f&&Mathf.Abs(h.Absorbed-(float)stat["absorbed"])<.01f&&h.SkillCasts==(int)stat["skills"],"Read model stats mismatch");} var changed=events[0].seq;events[0].seq=999;Check(!CombatTraceValidation.Events(manifest,events),"Corrupt sequence accepted");events[0].seq=changed; rows.Add(new{stage=stage.id,events=events.Length,won=battle.Won}); } string folder=Path.Combine(Repo,"Doc/Design/Combat/Runtime");Directory.CreateDirectory(folder);File.WriteAllText(Path.Combine(folder,"unity_domain.json"),JsonConvert.SerializeObject(new{status="pass",cases=files.Length,all20Stages=true,rows,corruptTraceRejected=true},Formatting.Indented));return "PASS 23 authoritative C++ traces -> C# read model: all 20 stages, three T2 capstones, exact final state/stats, corrupt sequences rejected"; } public static Task Visual()=>CaptureVisual(false); public static Task VisualSkills()=>CaptureVisual(true); static async Task CaptureVisual(bool signature) { Check(EditorApplication.isPlaying,"Use Play Mode");var data=JObject.Parse(File.ReadAllText(Path.Combine(Repo,"Tools/output/combat_runtime/traces/stage_20_t2_f0.json")));var snapshot=JsonUtility.FromJson(data["save"].ToString());var config=JsonUtility.FromJson(data["config"].ToString());var game=new PrototypeGame(PrototypeSave.BattleSnapshot(snapshot.level,snapshot.stages,snapshot.formation));var battle=new PrototypeBattle("isolated-trace-visual",game,config,20); battle.LoadTrace(JsonUtility.FromJson(data["manifest"].ToString()),data["events"].ToObject()); var arena=UnityEngine.Object.Instantiate(AssetDatabase.LoadAssetAtPath("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab"));var ui=UnityEngine.Object.Instantiate(AssetDatabase.LoadAssetAtPath("Assets/Bundles/UI/Prefab/UI_PrototypeBattle.prefab"));ui.name="UI_PrototypeBattle";ui.SetActive(false); var host=new GameObject("IsolatedAuthoritativeVisual");host.SetActive(false);var flow=host.AddComponent();Action set=(n,v)=>typeof(PrototypeFlow).GetField(n,BindingFlags.Instance|BindingFlags.NonPublic).SetValue(flow,v); try { set("_profile",new Func(()=>-1));set("_boundProfile",-1L);set("_game",game);set("_config",config);set("_battle",battle);set("_arena",arena);set("_settled",true);set("_speed",2);set("_page",Activator.CreateInstance(typeof(PrototypeFlow).GetNestedType("Panel",BindingFlags.NonPublic),new object[]{ui})); var heroes=Enumerable.Range(0,10).Select(i=>arena.transform.Find("Heroes/Hero"+i)).ToArray();var enemies=Enumerable.Range(0,9).Select(i=>arena.transform.Find("Enemies/Enemy"+i)).ToArray();var types=enemies.Select(s=>Enumerable.Range(0,5).Select(t=>s.Find("Type"+t)).ToArray()).ToArray();var t1=heroes.Select(h=>h.Find("Visual")).ToArray();var t2=heroes.Select(h=>h.Find("VisualT2")).ToArray(); set("_heroViews",heroes);set("_enemyViews",enemies);set("_enemyTypes",types);set("_heroT1Visuals",t1);set("_heroT2Visuals",t2);set("_heroAnimators",t1.Select(v=>v.GetComponentInChildren(true)).ToArray());set("_heroT2Animators",t2.Select(v=>v==null?null:v.GetComponentInChildren(true)).ToArray());set("_heroPulse",new float[10]);set("_heroSkillPulse",new float[10]);set("_enemyAnimators",types.Select(ts=>ts.Select(t=>t.GetComponentInChildren(true)).ToArray()).ToArray());set("_enemyModels",new PrototypeBattle.Enemy[9]);set("_enemyStrikes",new int[9]); foreach(var name in new[]{"_projectiles","_skillViews","_shieldViews"})set(name,Enumerable.Range(0,10).Select(i=>arena.transform.Find((name=="_projectiles"?"Projectiles/Shot":name=="_skillViews"?"SkillEffects/Skill":"SkillEffects/Shield")+i)).ToArray()); var fx=arena.GetComponentInChildren();set("_skillFeedback",fx);set("_enemyFeedback",arena.GetComponentInChildren()); Check(heroes.Concat(enemies).All(t=>t.Find("SkillMuzzle")!=null),"Missing authored muzzle");host.SetActive(true); var end=DateTime.UtcNow.AddSeconds(18);Func ready=()=>signature?fx.GetComponentsInChildren().Any(l=>l.enabled):fx.ActiveShots>0;while(!ready()&&DateTime.UtcNow0,"No authoritative visual cue");set("_paused",true);await Task.Delay(60); foreach(var ps in fx.GetComponentsInChildren().Where(ps=>ps.transform.parent.GetComponent()==null&&(ps.isPlaying||ps.isPaused))){ps.Simulate(.03f,true,false,true);ps.Pause(true);} var point=(Vector3)typeof(PrototypeFlow).GetMethod("CombatPoint",BindingFlags.Instance|BindingFlags.NonPublic).Invoke(flow,new object[]{1});var muzzle=heroes[1].Find("SkillMuzzle");Check(Vector3.Distance(point,muzzle.position)<.001f,"T2 point must use authored scaled muzzle"); var camera=arena.GetComponentInChildren();var rt=new RenderTexture(900,1200,24);var texture=new Texture2D(900,1200,TextureFormat.RGB24,false);var previous=RenderTexture.active; try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;texture.ReadPixels(new Rect(0,0,900,1200),0,0);texture.Apply();File.WriteAllBytes(Path.Combine(Repo,"AIClient/Assets/Screenshots/"+(signature?"partner_skills_battle":"battle_skills_anchors")+".png"),texture.EncodeToPNG());} finally{camera.targetTexture=null;RenderTexture.active=previous;rt.Release();UnityEngine.Object.Destroy(rt);UnityEngine.Object.Destroy(texture);} File.WriteAllText(Path.Combine(Repo,signature?"Doc/Design/Combat/PartnerSkills/battle_visual.json":"Doc/Design/Combat/Runtime/unity_anchors.json"),JsonConvert.SerializeObject(new{status="pass",authoritative=true,persistentMuzzles=19,t2ScaleApplied=true,flight=fx.ActiveShots>0,signature,effects=fx.PlayedEffects,networkAccountWrites=false},Formatting.Indented));return "PASS final T2 authored muzzles and authoritative flight; no account writes"; } finally{UnityEngine.Object.Destroy(host);UnityEngine.Object.Destroy(ui);UnityEngine.Object.Destroy(arena);} } public static async Task Begin(string username) { Check(EditorApplication.isPlaying,"Use Play Mode");await Until(()=>Flow?.Game!=null&&!Flow.IsBusy); SessionState.SetString("AIGame.Runtime.original",JsonUtility.ToJson(Flow.Game.State));SessionState.SetString("AIGame.Runtime.endpoint",Get("_httpBase"));SessionState.SetBool("AIGame.Runtime.sfx",PrototypeClientPrefs.Sfx); AuthResponse auth=null;bool done=false;string error=null;Flow.StartCoroutine(AuthApi.Login(Endpoint,username,"isolated-formal-heroes-password",(r,e)=>{auth=r;error=e;done=true;}));await Until(()=>done);Check(auth!=null,"Isolated login "+error); var session=new SessionData{Pid=auth.pid,Token=auth.token,Username=username};UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent().Hide(); Set("_session",new Func(()=>session));Set("_profile",new Func(()=>session.Pid));Set("_httpBase",Endpoint);Set("_boundProfile",0L);Flow.Handle("Home");await Until(()=>Flow.Game!=null&&!Flow.IsBusy&&Get("_boundProfile")==session.Pid); await ClickReady("Adventure");await Until(()=>Flow.Page=="UI_PrototypeStages");await ClickReady("Stage5");await Until(()=>Flow.Page=="UI_PrototypeFormation");await ClickReady("StartBattle");await Until(()=>Flow.Page=="UI_PrototypeBattle"&&Flow.Battle!=null); Check(Flow.Battle.Authoritative,"Client fell back to local simulation");var remote=Get("_reply").battle;Check(remote.snapshot.rulesVersion==3,"Missing rule version");SessionState.SetString("AIGame.Runtime.battle",remote.id);SessionState.SetString("AIGame.Runtime.trace",remote.trace.traceHash); var fx=Get("_skillFeedback");Check(fx!=null&&fx.EffectKinds==15&&fx.ProfileCount==10,"Missing authored feedback");PrototypeClientPrefs.Sfx=true; await Until(()=>fx.ActiveShots>0);await ClickReady("Pause");float clock=Flow.Battle.Time;await Task.Delay(350);Check(clock==Flow.Battle.Time,"Pause did not freeze data"); PrototypeClientPrefs.Sfx=false;await Task.Delay(50);Check(fx.GetComponentsInChildren().All(a=>!a.isPlaying),"Mute not applied");PrototypeClientPrefs.Sfx=true; await Capture();return "PASS real HTTP begin + full paged hash validation + T2 stage 5, flight/pause/mute; paused for isolated server restart"; } public static async Task ResumeAndFinish() { Check(EditorApplication.isPlaying&&Get("_httpBase")==Endpoint,"Expected isolated battle"); Call("Home");Flow.StartCoroutine((IEnumerator)Call("Synchronize"));await Until(()=>Flow.Battle!=null&&Flow.Page=="UI_PrototypeBattle"&&!Flow.IsBusy); var remote=Get("_reply").battle;Check(remote.id==SessionState.GetString("AIGame.Runtime.battle","")&&remote.trace.traceHash==SessionState.GetString("AIGame.Runtime.trace",""),"Restart regenerated pending battle"); Check(Flow.Battle.Authoritative,"Resume lost trace mode");await ClickReady("Speed");Check(Get("_speed")==2,"2x toggle failed"); var fx=Get("_skillFeedback");bool shot=false,shield=false,attack=false;var until=DateTime.UtcNow.AddSeconds(55); while(!Flow.Battle.Finished&&DateTime.UtcNow0;shield|=fx.ActiveShields>0;attack|=Flow.Battle.Enemies.Any(e=>e.Strikes>0); await Task.Delay(25); } Check(Flow.Battle.Finished&&Flow.Battle.TraceFinalStateMatches,"Client final state differs from authoritative trace");Check(shot&&shield&&attack,"Incomplete visible fight"); await Until(()=>!Flow.IsBusy&&string.IsNullOrEmpty(Flow.Game.State.pendingBattle));var receipt=Get("_reply").result;Check(receipt!=null&&receipt.won==Flow.Battle.Won,"Server result mismatch"); foreach(var stat in receipt.stats){var hero=Flow.Battle.Heroes.Single(h=>h.Pet==stat.pet);Check(Mathf.Abs(hero.DamageDealt-stat.damage)<.01f&&Mathf.Abs(hero.Healing-stat.healing)<.01f&&Mathf.Abs(hero.Absorbed-stat.absorbed)<.01f&&hero.SkillCasts==stat.skills,"Stats mismatch for "+stat.pet);} Check(fx.ActiveShots==0&&fx.ActiveCharges==0&&fx.ActiveShields==0,"End leaked presentation leases"); string folder=Path.Combine(Repo,"Doc/Design/Combat/Runtime");Directory.CreateDirectory(folder);File.WriteAllText(Path.Combine(folder,"unity_network.json"),JsonConvert.SerializeObject(new{status="pass",endpoint=Endpoint,stage=5,tier=2,authoritative=true,hashVerified=true,serverRestartRecovery=true,pause=true,mute=true,speed2=true,flight=shot,shield,enemyAttack=attack,finalStateExact=true,statsMatch=true,events=Flow.Battle.TraceEventsConsumed,won=receipt.won,sounds=fx.PlayedSounds,cleanup=true,realAccountWrites=false},Formatting.Indented)); return "PASS real paged HTTP battle after server restart: T2 stage 5, 2x, exact state/stats and server reward; screenshot battle_skills_network.png"; } public static async Task Cleanup() { PrototypeClientPrefs.Sfx=SessionState.GetBool("AIGame.Runtime.sfx",true);var net=UnityEngine.Object.FindObjectOfType();UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent().Hide(); Set("_session",new Func(()=>net.Session));Set("_profile",new Func(()=>net.Session?.Pid??0));Set("_httpBase",SessionState.GetString("AIGame.Runtime.endpoint","http://127.0.0.1:8080"));Set("_boundProfile",0L);Flow.Handle("Home");await Until(()=>Flow.Game!=null&&!Flow.IsBusy&&Get("_httpBase")!=Endpoint); var original=JsonUtility.FromJson(SessionState.GetString("AIGame.Runtime.original",""));var actual=Flow.Game.State;Check(actual.coins==original.coins&&actual.candy==original.candy&&actual.level==original.level&&actual.shots==original.shots&&actual.stages.SequenceEqual(original.stages)&&actual.formation.SequenceEqual(original.formation),"Real account changed"); return "Restored real account and preferences; wallet, level, roster, formation unchanged"; } }