Files

287 lines
21 KiB
C#

using System;
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 TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>Unseeded first-day acceptance via actual EventSystem clicks and isolated HTTP.</summary>
public static class VerifyFirstFive
{
const string Endpoint = "http://127.0.0.1:18085";
const string Key = "AIGame.FirstFive.";
static string Repo => Path.GetFullPath(Path.Combine(Application.dataPath, "../.."));
static string Output => Path.Combine(Repo, "Tools/output/first_five");
static PrototypeFlow Flow => UnityEngine.Object.FindObjectOfType<PrototypeFlow>();
static T Get<T>(string field) => (T)typeof(PrototypeFlow).GetField(field, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(Flow);
static void Set(string field, object value) => typeof(PrototypeFlow).GetField(field, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(Flow, value);
static void Check(bool value, string message) { if (!value) throw new Exception(message); }
static async Task Until(Func<bool> condition, int seconds = 35)
{
var end = DateTime.UtcNow.AddSeconds(seconds);
while (!condition()) { Check(DateTime.UtcNow < end, "Timeout: " + Flow?.Page); await Task.Delay(75); }
}
static Button Find(string name) => UnityEngine.Object.FindObjectsOfType<Button>().SingleOrDefault(b => b.name == "Btn_" + name && b.gameObject.activeInHierarchy);
static async Task Click(string name)
{
await Until(() => Find(name) != null && Find(name).interactable);
Button b = null; PointerEventData data = null;
await Until(() => {
Canvas.ForceUpdateCanvases(); b = Find(name); if (b == null || !b.interactable) return false;
var rect = b.GetComponent<RectTransform>();
data = 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(data, hits);
return hits.Count > 0 && ExecuteEvents.GetEventHandler<IPointerClickHandler>(hits[0].gameObject) == b.gameObject;
}, 8);
ExecuteEvents.Execute(b.gameObject, data, ExecuteEvents.pointerClickHandler);
await Task.Delay(100);
}
static string Text(string name) => UnityEngine.Object.FindObjectsOfType<TextMeshProUGUI>().FirstOrDefault(t => t.name == "Txt_" + name && t.gameObject.activeInHierarchy)?.text;
static JObject Wallet() => JObject.FromObject(new { Flow.Game.State.coins, Flow.Game.State.candy, Flow.Game.State.balls, Flow.Game.State.tickets, Flow.Game.State.level, Flow.Game.State.shots, Flow.Game.State.stages, Flow.Game.State.shards, Flow.Game.State.formation });
static void Save(JObject report, string step)
{
report["step"] = step; report["updatedUtc"] = DateTime.UtcNow.ToString("O");
if (Flow?.Game != null) report["current"] = Wallet();
Directory.CreateDirectory(Output);
File.WriteAllText(Path.Combine(Output, report.Value<string>("kind") == "guide" ? "ui_guide.json" : "ui_run.json"), report.ToString(Formatting.Indented));
}
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.Public | BindingFlags.Static).Invoke(null, new object[] { 1080, 1920, null, "Screenshots/first_five_" + name + ".png", false, 0, "screen" });
}
static async Task Home()
{
if (Flow.Page == "Home") return;
Check(Find("Ok") == null, "Dismiss the operation result before navigating");
await Click(Flow.Page == "UI_PrototypeBattle" ? "Home" : "Back");
await Until(() => Flow.Page == "Home" && !Flow.IsBusy);
}
static async Task Open(string button, string page)
{
await Home(); await Click(button); await Until(() => Flow.Page == "UI_Prototype" + page && !Flow.IsBusy);
}
static async Task Operation(string button)
{
var previous = Get<PrototypeReply>("_reply");
await Click(button); await Until(() => !Flow.IsBusy && !ReferenceEquals(Get<PrototypeReply>("_reply"), previous));
if (Find("Ok") != null) await Click("Ok");
Check(Get<PrototypeReply>("_reply").ok, "Rejected UI operation " + button);
}
static async Task Shot(JObject report)
{
if (Flow.Page != "UI_PrototypePinball") await Open("NavPinball", "Pinball");
int count = Flow.Game.State.shots;
Check(Flow.Game.State.balls > 0 && count < 60, "First-day shot budget exhausted");
var before = Wallet(); await Click("Fire");
await Until(() => !Flow.IsBusy && Flow.Game.State.shots == count + 1 && string.IsNullOrEmpty(Flow.Game.State.pendingShot), 25);
((JArray)report["operations"]).Add(new JObject { ["action"] = "shot", ["before"] = before, ["after"] = Wallet() });
Save(report, "shot " + Flow.Game.State.shots);
}
static async Task TrainTo(int level, JObject report)
{
while (Flow.Game.State.level < level)
{
int cost = Flow.Game.State.level * Get<PrototypeConfig>("_config").economy.trainCost;
while (Flow.Game.State.candy < cost) await Shot(report);
await Open("NavPets", "Pets");
var before = Wallet(); await Operation("Train");
Check(Flow.Game.State.level == (int)before["level"] + 1 && Flow.Game.State.candy == (int)before["candy"] - cost, "Training cost mismatch");
((JArray)report["operations"]).Add(new JObject { ["action"] = "train", ["before"] = before, ["after"] = Wallet() });
Save(report, "trained " + Flow.Game.State.level);
}
}
public static string Begin()
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
Check(!SessionState.GetBool(Key + "running", false), "Acceptance already running");
SessionState.SetBool(Key + "running", true);
_ = Run(); return "Started isolated first-five UI acceptance; progress in Tools/output/first_five/ui_run.json";
}
static async Task Run()
{
var report = new JObject { ["status"] = "running", ["endpoint"] = Endpoint, ["seededResources"] = false, ["startedUtc"] = DateTime.UtcNow.ToString("O"), ["operations"] = new JArray(), ["battles"] = new JArray() };
try
{
await Until(() => Flow?.Game != null && !Flow.IsBusy);
Check(string.IsNullOrEmpty(Flow.Game.State.pendingShot) && string.IsNullOrEmpty(Flow.Game.State.pendingBattle) && string.IsNullOrEmpty(Flow.Game.State.pendingFish), "Leave existing activity intact before acceptance");
SessionState.SetString(Key + "original", JsonUtility.ToJson(Flow.Game.State));
SessionState.SetString(Key + "endpoint", Get<string>("_httpBase"));
AuthResponse auth = null; string error = null; bool done = false;
string username = "firstfive_" + Guid.NewGuid().ToString("N");
Flow.StartCoroutine(AuthApi.Register(Endpoint, username, "isolated-first-five-password", (r, e) => { auth = r; error = e; done = true; }));
await Until(() => done); Check(auth != null, "Register: " + error);
var session = new SessionData { Pid = auth.pid, Token = auth.token, Username = username };
report["account"] = username; SessionState.SetInt(Key + "pid", (int)auth.pid);
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 && Get<long>("_boundProfile") == session.Pid);
Check(Flow.Game.State.balls == 20 && Flow.Game.State.coins == 0 && Flow.Game.State.candy == 0 && Flow.Game.State.level == 1 && Flow.Game.State.stages.Count(s => s > 0) == 2, "Not a genuine fresh account");
Check(Text("Task") == "发射第一枚弹珠", "Missing initial guide");
report["initial"] = Wallet(); Save(report, "fresh account");
await Shot(report); Check(Flow.Game.State.tickets == 1, "First-shot ticket missing");
await Click("Capture"); await Until(() => Flow.Page == "UI_PrototypeCapture");
await Click("Capture"); await Until(() => !Flow.IsBusy && Flow.Game.State.captureTutorialDone);
Check(Flow.Game.State.stages[2] == 1 && Flow.Game.State.tickets == 0, "Tutorial capture did not grant BudBud");
await Click("Ok"); await Until(() => Flow.Page == "UI_PrototypePets"); await Home();
await TrainTo(2, report);
for (int stage = 1; stage <= 5; stage++)
{
await TrainTo(Math.Max(2, stage), report);
await Open("Adventure", "Stages"); await Click("Stage" + stage); await Until(() => Flow.Page == "UI_PrototypeFormation" && !Flow.IsBusy);
await Operation("AutoDeploy");
var before = Wallet(); Save(report, "formation " + stage); Capture("formation" + stage);
await Click("StartBattle"); await Until(() => Flow.Page == "UI_PrototypeBattle" && Flow.Battle != null, 45);
Check(Flow.Battle.Authoritative && Get<PrototypeReply>("_reply").battle.snapshot.rulesVersion == 3, "Old combat path used");
var remote = Get<PrototypeReply>("_reply").battle;
var battle = new JObject { ["stage"] = stage, ["before"] = before, ["traceHash"] = remote.trace.traceHash, ["events"] = remote.trace.eventCount };
((JArray)report["battles"]).Add(battle); Save(report, "battle " + stage);
await Click("Speed");
await Until(() => Flow.Battle.Finished && !Flow.IsBusy && string.IsNullOrEmpty(Flow.Game.State.pendingBattle), 100);
Check(Flow.Battle.TraceFinalStateMatches, "Authority replay final state mismatch");
battle["won"] = Flow.Battle.Won; battle["seconds"] = Flow.Battle.Time; battle["after"] = Wallet(); battle["resultText"] = Text("ResultInfo");
battle["receipt"] = JToken.FromObject(Get<PrototypeReply>("_reply").result); Capture("result" + stage); Save(report, "result " + stage);
Check(Flow.Battle.Won, "Normal starter roster lost stage " + stage + " at level " + Flow.Game.State.level);
Check(Flow.Game.State.clearedStages[stage - 1] == 1, "Clear flag missing");
var reward = Get<PrototypeConfig>("_config").Stage(stage).reward;
Check(Flow.Game.State.coins == (int)before["coins"] + reward.coins && Flow.Game.State.candy == (int)before["candy"] + reward.candy && Flow.Game.State.balls == (int)before["balls"] + reward.balls, "First-clear reward mismatch");
await Home();
if (stage == 1)
{
Check(Text("Task") == "领取首通目标奖励", "Goal guide missing");
await Open("Tasks", "Tasks"); await Operation("ClaimGoal"); await Home();
await Open("Mail", "Mail"); await Operation("ClaimAllMail"); await Home();
await Open("Daily", "Signin"); await Operation("Signin0"); await Home();
await Open("NavShop", "Shop"); await Operation("BuyGoods0"); await Home();
await Open("Rewards", "Chest"); await Operation("OpenChest0"); await Home();
Save(report, "normal first-day support rewards claimed");
}
}
report["status"] = "pass"; report["final"] = Wallet();
report["note"] = "Fresh account; only initial two pets plus guaranteed tutorial BudBud, T1 throughout; actual UI, protocol1/rules3; no save seeding, no injected wallet, clock or random draw.";
Save(report, "complete");
}
catch (Exception error)
{
report["status"] = "failed"; report["error"] = error.ToString(); report["page"] = Flow?.Page; report["dialog"] = Text("Message");
Save(report, "failed");
}
finally { SessionState.SetBool(Key + "running", false); }
}
public static async Task<string> Cleanup()
{
Check(!SessionState.GetBool(Key + "running", false), "Wait for acceptance completion before 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(Key + "endpoint", "http://127.0.0.1:8080")); Set("_boundProfile", 0L);
Flow.Handle("Home"); await Until(() => Flow.Game != null && !Flow.IsBusy && Get<string>("_httpBase") != Endpoint && Get<long>("_boundProfile") == net.Session.Pid);
var before = JsonUtility.FromJson<PrototypeSave>(SessionState.GetString(Key + "original", "")); var after = Flow.Game.State;
Check(after.coins == before.coins && after.candy == before.candy && after.level == before.level && after.shots == before.shots && after.stages.SequenceEqual(before.stages) && after.formation.SequenceEqual(before.formation), "Original account changed");
PlayerPrefs.DeleteKey("AIGame.G1.pending." + Uri.EscapeDataString(Endpoint) + "." + SessionState.GetInt(Key + "pid", 0));
return "PASS original account and endpoint restored; coins/candy/level/shots/roster/formation unchanged";
}
public static string BeginGuide()
{
Check(EditorApplication.isPlaying && !SessionState.GetBool(Key + "running", false), "Need idle Play Mode");
SessionState.SetBool(Key + "running", true); _ = Guide();
return "Started real fresh-account guide shortage / ready / trained checks";
}
public static string AnimationLifecycle()
{
Check(EditorApplication.isPlaying, "Use Play Mode");
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab");
var arena = UnityEngine.Object.Instantiate(prefab); arena.name = "FirstFiveAnimationLifecycle";
var messages = new List<string>();
Application.LogCallback observe = (message, stack, kind) => { if (kind == LogType.Warning || kind == LogType.Error || kind == LogType.Exception) messages.Add(message); };
try
{
foreach (var camera in arena.GetComponentsInChildren<Camera>(true)) camera.enabled = false;
var fx = arena.GetComponentInChildren<BattleSkillPresentation>(true);
var hero = arena.transform.Find("Heroes/Hero0"); var visual = hero.Find("Visual");
var animator = visual.GetComponentInChildren<Animator>(true);
Application.logMessageReceived += observe;
arena.SetActive(false); fx.Animate(animator, true, 0, 1, -1, 0, 0, false);
arena.SetActive(true); hero.gameObject.SetActive(true); visual.gameObject.SetActive(true); animator.enabled = false;
fx.Animate(animator, true, 0, 2, -1, 0, 0, false);
animator.enabled = true; fx.Animate(animator, true, 0, 3, -1, 0, 0, false);
Check(animator.GetCurrentAnimatorStateInfo(0).IsName("Idle"), "Re-enabled animator did not resume sampling");
hero.gameObject.SetActive(false); fx.Animate(animator, true, 0, 4, -1, 0, 0, false);
Check(messages.Count == 0, "Animation lifecycle logs: " + string.Join("; ", messages));
Directory.CreateDirectory(Output);
File.WriteAllText(Path.Combine(Output, "animation_lifecycle.json"), JsonConvert.SerializeObject(new { status = "pass", inactiveParent = true, disabledAnimator = true, reenabledSampling = true, hiddenUnit = true, warnings = 0 }));
return "PASS pooled animation: inactive/disabled/hidden skip silently; re-enabled samples Idle";
}
finally { Application.logMessageReceived -= observe; UnityEngine.Object.Destroy(arena); }
}
static async Task Guide()
{
var report = new JObject { ["kind"] = "guide", ["status"] = "running", ["operations"] = new JArray() };
try
{
await Until(() => Flow?.Game != null && !Flow.IsBusy);
SessionState.SetString(Key + "original", JsonUtility.ToJson(Flow.Game.State));
SessionState.SetString(Key + "endpoint", Get<string>("_httpBase"));
AuthResponse auth = null; bool done = false;
Flow.StartCoroutine(AuthApi.Register(Endpoint, "firstguide_" + Guid.NewGuid().ToString("N"), "isolated-first-five-password", (r, e) => { auth = r; done = true; }));
await Until(() => done); Check(auth != null, "Guide register failed");
var session = new SessionData { Pid = auth.pid, Token = auth.token }; SessionState.SetInt(Key + "pid", (int)auth.pid);
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 && Get<long>("_boundProfile") == session.Pid);
await Shot(report); await Click("Capture"); await Until(() => Flow.Page == "UI_PrototypeCapture");
await Click("Capture"); await Until(() => !Flow.IsBusy && Flow.Game.State.captureTutorialDone); await Click("Ok"); await Until(() => Flow.Page == "UI_PrototypePets"); await Home();
Check(Flow.Game.State.candy == 20 && Text("Task") == "发射弹珠,补足训练糖果", "Shortage guide was " + Text("Task"));
report["shortage"] = Text("Task"); Capture("guide_shortage");
await Shot(report); await Home(); Check(Flow.Game.State.candy == 40 && Text("Task") == "用糖果做一次共享训练", "Ready guide was " + Text("Task"));
report["ready"] = Text("Task"); Capture("guide_ready");
await Open("NavPets", "Pets"); await Operation("Train"); await Home();
Check(Flow.Game.State.level == 2 && Text("Task") == "布阵并完成教学关 1-1", "Trained guide was " + Text("Task"));
report["trained"] = Text("Task"); report["status"] = "pass"; Save(report, "guide complete");
}
catch (Exception error) { report["status"] = "failed"; report["error"] = error.ToString(); Save(report, "guide failed"); }
finally { SessionState.SetBool(Key + "running", false); }
}
public static string BeginBattleSmoke()
{
Check(EditorApplication.isPlaying && Get<string>("_httpBase") == Endpoint && !SessionState.GetBool(Key + "running", false), "Need completed isolated guide");
SessionState.SetBool(Key + "running", true); _ = BattleSmoke(); return "Started post-fix real HTTP battle and warning check";
}
static async Task BattleSmoke()
{
var report = new JObject { ["status"] = "running" }; var messages = new List<string>();
Application.LogCallback observe = (message, stack, kind) => {
if (kind == LogType.Error || kind == LogType.Exception || (kind == LogType.Warning && message.Contains("Animator"))) messages.Add(message);
};
Application.logMessageReceived += observe;
try
{
await Open("Adventure", "Stages"); await Click("Stage1"); await Until(() => Flow.Page == "UI_PrototypeFormation");
await Click("StartBattle"); await Until(() => Flow.Page == "UI_PrototypeBattle" && Flow.Battle != null);
Check(Flow.Battle.Authoritative, "Missing authority"); await Click("Speed");
await Until(() => Flow.Battle.Finished && !Flow.IsBusy && string.IsNullOrEmpty(Flow.Game.State.pendingBattle), 90);
Check(Flow.Battle.Won && Flow.Battle.TraceFinalStateMatches, "Post-fix battle failed");
Check(messages.Count == 0, "Post-fix errors/animator warnings: " + string.Join("; ", messages.Distinct()));
report["status"] = "pass"; report["authoritative"] = true; report["finalStateExact"] = true; report["animatorWarnings"] = 0; report["errors"] = 0;
}
catch (Exception error) { report["status"] = "failed"; report["error"] = error.ToString(); }
finally
{
Application.logMessageReceived -= observe; SessionState.SetBool(Key + "running", false);
File.WriteAllText(Path.Combine(Output, "postfix_battle.json"), report.ToString(Formatting.Indented));
}
}
}