249 lines
19 KiB
C#
249 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
using HotUpdate.Gameplay;
|
|
using HotUpdate.Net;
|
|
using HotUpdate.UI;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
public static class VerifyG1
|
|
{
|
|
const string Endpoint = "http://127.0.0.1:18081";
|
|
static PrototypeFlow Flow => UnityEngine.Object.FindObjectOfType<PrototypeFlow>();
|
|
static void Check(bool condition, string message) { if (!condition) throw new Exception(message); }
|
|
static void Set(string field, object value) => typeof(PrototypeFlow).GetField(field, BindingFlags.NonPublic | BindingFlags.Instance).SetValue(Flow, value);
|
|
static T Get<T>(string field) => (T)typeof(PrototypeFlow).GetField(field, BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Flow);
|
|
static async Task Until(Func<bool> condition, int seconds = 20)
|
|
{
|
|
var deadline = DateTime.UtcNow.AddSeconds(seconds);
|
|
while (!condition()) { Check(DateTime.UtcNow < deadline, "G1 condition timed out: " + Flow?.Page); await Task.Delay(100); }
|
|
}
|
|
static void Click(string name)
|
|
{
|
|
var b = UnityEngine.Object.FindObjectsOfType<Button>().Single(x => x.name == "Btn_" + name && x.gameObject.activeInHierarchy);
|
|
Check(b.interactable, "Disabled button " + name); Canvas.ForceUpdateCanvases();
|
|
var rect = b.GetComponent<RectTransform>();
|
|
var pointer = new PointerEventData(EventSystem.current) { position = RectTransformUtility.WorldToScreenPoint(null, rect.TransformPoint(rect.rect.center)), button = PointerEventData.InputButton.Left };
|
|
var hits = new List<RaycastResult>(); EventSystem.current.RaycastAll(pointer, hits);
|
|
Check(hits.Count > 0 && ExecuteEvents.GetEventHandler<IPointerClickHandler>(hits[0].gameObject) == b.gameObject, "Raycast blocked " + name + ": " + string.Join(",",hits.Take(3).Select(h=>h.gameObject.name)));
|
|
ExecuteEvents.Execute(b.gameObject, pointer, ExecuteEvents.pointerClickHandler);
|
|
}
|
|
static void Capture(string name)
|
|
{
|
|
var type=AppDomain.CurrentDomain.GetAssemblies().Select(a=>a.GetType("Unity.Pipeline.Editor.Commands.Capture.CaptureCommands")).First(t=>t!=null);
|
|
type.GetMethod("CaptureGameView",BindingFlags.Static|BindingFlags.Public).Invoke(null,new object[]{1080,1920,null,"Screenshots/progression_"+name+".png",false,0,"screen"});
|
|
}
|
|
static async Task Shot()
|
|
{
|
|
int shots = Flow.Game.State.shots;
|
|
Click("Fire");
|
|
await Until(() => !Flow.IsBusy && Flow.Game.State.shots == shots + 1 && string.IsNullOrEmpty(Flow.Game.State.pendingShot), 15);
|
|
}
|
|
public static async Task<string> LiveStart()
|
|
{
|
|
Check(EditorApplication.isPlaying, "Enter Play Mode first");
|
|
await Until(() => Flow?.Game != null && !Flow.IsBusy);
|
|
SessionState.SetString("AIGame.G1.original", JsonUtility.ToJson(Flow.Game.State));
|
|
SessionState.SetString("AIGame.G1.originalEndpoint", Get<string>("_httpBase"));
|
|
AuthResponse account = null; string error = null; bool complete = false;
|
|
string user = "g1live_" + Guid.NewGuid().ToString("N");
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(Application.dataPath, "../../Tools/output/g1-live/account.txt"), user);
|
|
Flow.StartCoroutine(AuthApi.Register(Endpoint, user, "isolated-g1-test-password", (r,e) => { account = r; error = e; complete = true; }));
|
|
await Until(() => complete); Check(account != null, "Isolated register failed " + error);
|
|
var session = new SessionData { Pid = account.pid, Token = account.token, Username = user };
|
|
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
|
|
Set("_session", new Func<SessionData>(() => session)); Set("_profile", new Func<long>(() => session.Pid)); Set("_httpBase", Endpoint); Set("_boundProfile", 0L);
|
|
// New isolated DB/account, no old verification command may be replayed into it.
|
|
PlayerPrefs.DeleteKey("AIGame.G1.pending." + Uri.EscapeDataString(Endpoint) + "." + session.Pid);
|
|
Flow.Handle("Home"); await Until(() => Flow.Game != null && !Flow.IsBusy);
|
|
Check(Flow.Game.State.balls == 20 && Flow.Game.State.coins == 0, "Fresh server wallet incorrect");
|
|
Click("Adventure"); await Until(()=>Flow.Page=="UI_PrototypeStages");
|
|
Check(!UnityEngine.Object.FindObjectsOfType<Button>().Single(b=>b.name=="Btn_Stage2").interactable,"Stage2 unlocked before first clear");
|
|
Capture("stages_locked"); Click("Stage1"); await Until(()=>Flow.Page=="UI_PrototypeFormation");
|
|
Click("Back"); await Until(()=>Flow.Page=="Home"); await Task.Delay(150);
|
|
Click("NavPinball"); await Until(() => Flow.Page == "UI_PrototypePinball");
|
|
Click("AimRight"); await Shot();
|
|
for (int i=0; i<4 && Flow.Game.State.candy < 30; i++) await Shot();
|
|
Check(Flow.Game.State.candy >= 30, "No training currency");
|
|
Click("Capture"); await Until(() => Flow.Page == "UI_PrototypeCapture"); Capture("capture_tutorial"); Click("Capture"); await Until(() => !Flow.IsBusy && Flow.Game.State.stages[2] == 1);
|
|
Click("Ok"); await Until(() => Flow.Page == "UI_PrototypePets");
|
|
Click("Train"); await Until(() => !Flow.IsBusy && Flow.Game.State.level == 2);
|
|
Capture("pets");
|
|
Click("ToFormation"); await Until(() => Flow.Page == "UI_PrototypeFormation");
|
|
Click("Pet1"); Click("Slot0"); await Until(() => !Flow.IsBusy && Flow.Game.State.formation[0] == 1);
|
|
Check(Flow.Game.State.formation[1] == 0, "Formation lost occupant");
|
|
Click("AutoDeploy"); await Until(() => !Flow.IsBusy && Flow.Game.State.formation[0] == 0);
|
|
Capture("formation1");
|
|
SessionState.SetInt("AIGame.G1.coins", Flow.Game.State.coins); SessionState.SetInt("AIGame.G1.candy", Flow.Game.State.candy); SessionState.SetInt("AIGame.G1.balls", Flow.Game.State.balls);
|
|
Click("StartBattle"); await Until(() => Flow.Page == "UI_PrototypeBattle" && Flow.Battle != null);
|
|
Check(Flow.Battle.Id == Flow.Game.State.pendingBattle, "Battle not server-authorized");
|
|
Click("Pause"); float time = Flow.Battle.Time; await Task.Delay(200); Check(Flow.Battle.Time == time, "Pause advanced simulation");
|
|
Click("Speed"); Click("Pause"); await Until(() => Flow.Battle.Time >= 14);
|
|
return "PASS G1 UI: authenticated snapshot, server trajectory, capture, training, formation swap, server battle ticket, pause and 2x. Isolated battle running.";
|
|
}
|
|
public static string LiveFinish()
|
|
{
|
|
if (Flow?.Battle == null || !Flow.Battle.Finished || Flow.IsBusy || !Flow.Game.State.cleared)
|
|
return "RUNNING: battle time=" + Flow?.Battle?.Time + "; busy=" + Flow?.IsBusy;
|
|
Check(Flow.Game.State.coins == SessionState.GetInt("AIGame.G1.coins",0)+80 && Flow.Game.State.candy == SessionState.GetInt("AIGame.G1.candy",0)+40 && Flow.Game.State.balls == SessionState.GetInt("AIGame.G1.balls",0)+3, "First clear server reward mismatch");
|
|
Check(string.IsNullOrEmpty(Flow.Game.State.pendingBattle), "Battle not settled");
|
|
Capture("result1");
|
|
return "PASS G1 live battle: server-authoritative victory, exact 80 coins / 40 candy / 3 balls, persistent settlement and authored result UI.";
|
|
}
|
|
public static async Task<string> Recovery()
|
|
{
|
|
Check(Flow.Game.State.cleared && !Flow.IsBusy, "Finish battle first");
|
|
if (Flow.Page != "Home") Click("Home");
|
|
if (!Flow.Game.State.goalClaimed)
|
|
{
|
|
await Task.Delay(150); await Until(()=>!Flow.IsBusy);
|
|
int beforeClaim = Flow.Game.State.balls;
|
|
await Task.Delay(100); Click("Tasks"); await Until(() => Flow.Page == "UI_PrototypeTasks"); Click("ClaimGoal");
|
|
await Until(() => !Flow.IsBusy && Flow.Game.State.goalClaimed);
|
|
Click("Ok"); Click("Back"); await Until(() => Flow.Page == "Home");
|
|
Check(Flow.Game.State.balls == beforeClaim + 5, "Goal reward mismatch");
|
|
}
|
|
int balls = Flow.Game.State.balls, shots = Flow.Game.State.shots;
|
|
// Simulate a crash after the server committed a request but before the client cleared its journal.
|
|
var api = Get<PrototypeApi>("_api"); var command = api.Command("shot.begin", Flow.Game.State.revision, aim: -15);
|
|
PrototypeReply response = null; string error = null;
|
|
Flow.StartCoroutine(api.Send(command, (r,e) => { response = r; error = e; }));
|
|
await Until(() => response != null || error != null); Check(response?.ok == true, "Recovery shot failed " + error);
|
|
string key = "AIGame.G1.pending." + Uri.EscapeDataString(Endpoint) + "." + Get<Func<SessionData>>("_session")().Pid;
|
|
PlayerPrefs.SetString(key, JsonUtility.ToJson(command)); PlayerPrefs.Save();
|
|
Set("_needsSync", true); Flow.Handle("Home");
|
|
await Until(() => Flow.IsShooting && Flow.Page == "UI_PrototypePinball");
|
|
Check(Flow.Game.State.balls == balls - 1, "Recovery charged twice");
|
|
// Reattach once more while the server activity is pending, with no journal this time.
|
|
Set("_boundProfile", 0L); Flow.Handle("Home");
|
|
await Until(() => !Flow.IsBusy && Flow.Game.State.shots == shots+1 && string.IsNullOrEmpty(Flow.Game.State.pendingShot),15);
|
|
Check(Flow.Game.State.balls == balls-1 && !PlayerPrefs.HasKey(key), "Recovery debit or journal mismatch");
|
|
Click("Back");
|
|
return "PASS G1 recovery: one goal reward; lost-ack replay; scene/session reattach during flight; one ball debit and one settlement; journal cleared.";
|
|
}
|
|
public static async Task<string> Cleanup()
|
|
{
|
|
var net = UnityEngine.Object.FindObjectOfType<GameNet>();
|
|
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
|
|
Set("_session", new Func<SessionData>(() => net.Session)); Set("_profile", new Func<long>(() => net.Session?.Pid ?? 0));
|
|
Set("_httpBase", SessionState.GetString("AIGame.G1.originalEndpoint", "http://127.0.0.1:8080")); Set("_boundProfile", 0L);
|
|
Flow.Handle("Home"); await Until(() => Flow.Game != null && !Flow.IsBusy);
|
|
var original = JsonUtility.FromJson<PrototypeSave>(SessionState.GetString("AIGame.G1.original", ""));
|
|
Check(Flow.Game.State.coins == original.coins && Flow.Game.State.candy == original.candy && Flow.Game.State.shots == original.shots && Flow.Game.State.level == original.level, "Real account was changed by test");
|
|
return "PASS cleanup: real account restored; no gameplay operations were sent to it. All live verification used isolated AIServer :18081.";
|
|
}
|
|
public static async Task<string> RenewAuthentication()
|
|
{
|
|
var current = Get<Func<SessionData>>("_session")();
|
|
var testSession = new SessionData { Pid = current.Pid, Username = current.Username, Token = "expired-test-token" };
|
|
int renewals = 0; string loginError = null;
|
|
var api = new PrototypeApi(Endpoint, current.Pid, () => testSession, () => {
|
|
renewals++;
|
|
Flow.StartCoroutine(AuthApi.Login(Endpoint, current.Username, "isolated-g1-test-password", (r,e) => {
|
|
loginError = e;
|
|
if (r != null) testSession = new SessionData { Pid = r.pid, Username = current.Username, Token = r.token };
|
|
}));
|
|
});
|
|
PrototypeReply reply = null; string error = null; bool done = false;
|
|
var command = api.Command("goal.claim", Flow.Game.State.revision);
|
|
Flow.StartCoroutine(api.Send(command, (r,e) => { reply = r; error = e; done = true; }));
|
|
await Until(() => done, 25);
|
|
Check(reply != null && reply.error == "goal_claimed" && renewals == 1 && error == null && loginError == null && !api.HasPending, "401 renewal failed");
|
|
Check(reply.state.balls == Flow.Game.State.balls && reply.state.coins == Flow.Game.State.coins, "401 retry changed balances");
|
|
// A previous failed login can leave Session null; retry must still restart authentication.
|
|
testSession = null; reply = null; error = null; done = false;
|
|
Flow.StartCoroutine(api.Read((r,e) => { reply = r; error = e; done = true; }));
|
|
await Until(() => done,25);
|
|
Check(reply != null && reply.ok && renewals == 2 && error == null, "Null-session authentication retry failed");
|
|
return "PASS client 401/null-session recovery: renewed same guest identity, retried durable command once, kept balances, cleared acknowledged journal, recovered after missing session.";
|
|
}
|
|
public static async Task<string> AttachExisting(string username)
|
|
{
|
|
await Until(() => Flow?.Game != null && !Flow.IsBusy);
|
|
AuthResponse account = null; string error = null; bool done = false;
|
|
Flow.StartCoroutine(AuthApi.Login(Endpoint, username, "isolated-g1-test-password", (r,e) => { account=r; error=e; done=true; }));
|
|
await Until(() => done); Check(account != null, "Test account login failed " + error);
|
|
var session = new SessionData { Pid=account.pid, Token=account.token, Username=username };
|
|
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
|
|
Set("_session",new Func<SessionData>(()=>session)); Set("_profile",new Func<long>(()=>session.Pid)); Set("_httpBase",Endpoint); Set("_boundProfile",0L);
|
|
Flow.Handle("Home"); await Until(()=>Flow.Game != null && !Flow.IsBusy);
|
|
return "Attached isolated verification account without changing wallet.";
|
|
}
|
|
public static Task<string> AttachPrevious() => AttachExisting(System.IO.File.ReadAllText(System.IO.Path.Combine(Application.dataPath, "../../Tools/output/g1-live/account.txt")).Trim());
|
|
public static async Task<string> AbandonRetry()
|
|
{
|
|
Check(!Flow.IsBusy, "Wait for sync");
|
|
int coins=Flow.Game.State.coins,candy=Flow.Game.State.candy,balls=Flow.Game.State.balls;
|
|
Flow.Handle("Adventure"); await Until(()=>Flow.Page=="UI_PrototypeStages");
|
|
Click("Stage1"); await Until(()=>Flow.Page=="UI_PrototypeFormation");
|
|
Click("StartBattle"); await Until(()=>Flow.Page=="UI_PrototypeBattle" && !Flow.IsBusy);
|
|
var id=Flow.Battle.Id;
|
|
// Simulate a stale presentation snapshot, forcing a durable refusal of the first abandon request.
|
|
Flow.Game.State.revision--;
|
|
Click("Abandon"); await Task.Delay(100); Click("Ok");
|
|
await Until(()=>Get<bool>("_needsSync") && !Get<bool>("_busy"));
|
|
Click("Ok");
|
|
await Until(()=>!Flow.IsBusy && Flow.Game.State.lastBattle==id && string.IsNullOrEmpty(Flow.Game.State.pendingBattle));
|
|
Check(Flow.Game.State.coins==coins && Flow.Game.State.candy==candy && Flow.Game.State.balls==balls,"Abandon retry awarded currency");
|
|
Check(Get<PrototypeReply>("_reply").result.won==false,"Abandon retry became a victory");
|
|
return "PASS stale-abandon recovery: first request refused; sync retained abandon intent; new request ended battle immediately without rewards.";
|
|
}
|
|
public static string FinalState()
|
|
{
|
|
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
|
|
Check(!EditorApplication.isPlaying && !EditorApplication.isCompiling, "Editor not idle");
|
|
Check(!scene.isDirty, "Scene has unsaved changes; do not discard user changes");
|
|
return "Editor stopped, compilation complete, active scene clean: " + scene.path;
|
|
}
|
|
public static async Task<string> NextStageStart()
|
|
{
|
|
Check(Flow.Game.State.clearedStages[0]==1&&!Flow.IsBusy,"Finish first stage");
|
|
Click("NextStage"); await Until(()=>Flow.Page=="UI_PrototypeFormation"&&Flow.Game.State.selectedStage==2&&!Flow.IsBusy);
|
|
Capture("formation2"); Click("ToStages"); await Until(()=>Flow.Page=="UI_PrototypeStages");
|
|
Check(Flow.Game.State.UnlockedStage==2,"Wrong unlocked stage");
|
|
Check(!UnityEngine.Object.FindObjectsOfType<Button>().Single(b=>b.name=="Btn_Stage3").interactable,"Stage3 unlocked too early");
|
|
Capture("stages_progress");Click("Stage2");await Until(()=>Flow.Page=="UI_PrototypeFormation"&&!Flow.IsBusy);
|
|
SessionState.SetInt("AIGame.G1.stage2Coins",Flow.Game.State.coins);SessionState.SetInt("AIGame.G1.stage2Candy",Flow.Game.State.candy);SessionState.SetInt("AIGame.G1.stage2Balls",Flow.Game.State.balls);
|
|
Click("StartBattle");await Until(()=>Flow.Page=="UI_PrototypeBattle"&&Flow.Battle?.StageId==2);
|
|
Check(Flow.Battle.Config.version==Get<PrototypeReply>("_reply").battle.snapshot.config.version,"Battle did not use frozen config");
|
|
Click("Speed");await Until(()=>Flow.Battle.Time>10);
|
|
return "PASS live progression: first clear unlocks only Stage2; NextStage opens saved Stage2 formation; map shows progress, Stage3 locked; grass-element battle running.";
|
|
}
|
|
public static string NextStageFinish()
|
|
{
|
|
if(Flow.Battle?.StageId!=2||!Flow.Battle.Finished||Flow.IsBusy||Flow.Game.State.clearedStages[1]!=1) return "RUNNING stage2 time="+Flow.Battle?.Time;
|
|
Check(Flow.Game.State.coins==SessionState.GetInt("AIGame.G1.stage2Coins",0)+90&&Flow.Game.State.candy==SessionState.GetInt("AIGame.G1.stage2Candy",0)+50&&Flow.Game.State.balls==SessionState.GetInt("AIGame.G1.stage2Balls",0)+3,"Stage2 configured reward mismatch");
|
|
Check(Flow.Game.State.UnlockedStage==3,"Stage3 not unlocked after second clear");Capture("result2");
|
|
return "PASS live Stage2: grass-element simulation wins; 90 coins / 50 candy / 3 balls awarded exactly; Stage3 unlocked.";
|
|
}
|
|
public static async Task<string> ResumeBattle()
|
|
{
|
|
await Until(()=>Flow.Battle!=null&&Flow.Page=="UI_PrototypeBattle");
|
|
if(Get<int>("_speed")==1) Click("Speed");
|
|
await Until(()=>Flow.Battle.Time>10);
|
|
return "Resumed server-persisted battle "+Flow.Battle.StageId+" at 2x without a new battle ticket.";
|
|
}
|
|
public static string LiveState() => "page="+Flow?.Page+" state="+JsonUtility.ToJson(Flow?.Game?.State);
|
|
public static async Task<string> RepeatFirstStage()
|
|
{
|
|
Check(Flow.Game.State.clearedStages[0]==1,"First clear not persisted");
|
|
Check(Flow.Game.State.coins==SessionState.GetInt("AIGame.G1.coins",0)+80&&Flow.Game.State.candy==SessionState.GetInt("AIGame.G1.candy",0)+40&&Flow.Game.State.balls==SessionState.GetInt("AIGame.G1.balls",0)+3,"Persisted first clear reward mismatch");
|
|
Flow.Handle("Adventure"); await Until(()=>Flow.Page=="UI_PrototypeStages");
|
|
Click("Stage1"); await Until(()=>Flow.Page=="UI_PrototypeFormation"&&!Flow.IsBusy);
|
|
Click("StartBattle");await Until(()=>Flow.Page=="UI_PrototypeBattle"&&Flow.Battle!=null);
|
|
Click("Speed");await Until(()=>Flow.Battle.Time>10);
|
|
return "Persisted first clear reward confirmed after restart; repeating first stage to verify result/next-stage navigation.";
|
|
}
|
|
public static async Task<string> HomePreview()
|
|
{
|
|
if(Flow.Page!="Home") Click("Home");
|
|
await Task.Delay(150);await Until(()=>!Flow.IsBusy);Capture("home");
|
|
return "Captured camp HUD with live next-stage config and resource values.";
|
|
}
|
|
}
|