177 lines
16 KiB
C#
177 lines
16 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 TMPro;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
public static class VerifyMail
|
|
{
|
|
static PrototypeFlow Flow => UnityEngine.Object.FindObjectOfType<PrototypeFlow>();
|
|
static void Check(bool value, string message) { if (!value) throw new Exception(message); }
|
|
static T Get<T>(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 async Task Until(Func<bool> condition, int seconds = 20)
|
|
{ var end = DateTime.UtcNow.AddSeconds(seconds); while (!condition()) { Check(DateTime.UtcNow < end, "Mail timed out: " + Flow?.Page); await Task.Delay(100); } await Task.Delay(120); }
|
|
static Button Button(string name) => UnityEngine.Object.FindObjectsOfType<Button>().Single(b => b.name == "Btn_" + name && b.gameObject.activeInHierarchy);
|
|
static void Click(string name)
|
|
{
|
|
var b = Button(name); Check(b.interactable, "Disabled " + 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, "Blocked " + name);
|
|
ExecuteEvents.Execute(b.gameObject, pointer, ExecuteEvents.pointerClickHandler);
|
|
}
|
|
static async Task Capture(string name)
|
|
{
|
|
await Task.Delay(180);
|
|
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/mail_" + name + ".png", false, 0, "screen" });
|
|
}
|
|
static async Task Refresh()
|
|
{
|
|
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
|
|
Flow.Handle("Home"); Set("_needsSync", true); Flow.Handle("Home"); await Until(() => !Flow.IsBusy);
|
|
await Task.Delay(120); Click("Mail"); await Until(() => Flow.Page == "UI_PrototypeMail");
|
|
}
|
|
public static string Assets()
|
|
{
|
|
var p = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/UI_PrototypeMail.prefab"); Check(p != null, "Missing mail prefab");
|
|
Check(p.GetComponentsInChildren<TextMeshProUGUI>(true).All(t => t.font != null), "Missing mail font");
|
|
foreach (var i in p.GetComponentsInChildren<Image>(true).Where(i => i.type == Image.Type.Sliced)) Check(i.sprite != null && i.sprite.border.x > 0 && i.sprite.border.y > 0, "Invalid mail nine slice");
|
|
var scroll = p.GetComponentInChildren<ScrollRect>(true); Check(scroll != null && scroll.content != null && scroll.viewport != null, "Mail body scroll not bound");
|
|
var config = JsonUtility.FromJson<PrototypeConfig>(File.ReadAllText(Path.Combine(Application.dataPath, "../../Config/Gameplay/greenfield.json")));
|
|
Check(PrototypeConfig.Valid(config) && config.mail.initial.Length == 2, "Mail config invalid");
|
|
var oldJson = Newtonsoft.Json.Linq.JObject.Parse(JsonUtility.ToJson(config)); oldJson.Remove("mail");
|
|
Check(PrototypeConfig.Valid(JsonUtility.FromJson<PrototypeConfig>(oldJson.ToString())), "Pre-mail server/frozen config rejected");
|
|
var method = typeof(PrototypeApi).GetMethod("ValidMailReceipt", BindingFlags.Static | BindingFlags.NonPublic);
|
|
var request = new PrototypeCommand { action = "mail.claim", requestId = "test", mailId = "a" };
|
|
var reply = new PrototypeReply { action = request.action, ok = true, result = new PrototypeResult { id = "test", mailId = "a", claimedCount = 1, reward = new StageReward { balls = 5 } } };
|
|
Check((bool)method.Invoke(null, new object[] { reply, request }), "Valid mail receipt rejected");
|
|
var partial = Newtonsoft.Json.Linq.JObject.Parse(JsonUtility.ToJson(reply)); ((Newtonsoft.Json.Linq.JObject)partial["result"]).Remove("reward");
|
|
Check(!(bool)method.Invoke(null, new object[] { JsonUtility.FromJson<PrototypeReply>(partial.ToString()), request }), "Missing JSON reward acknowledged as empty nested object");
|
|
reply.result.reward = null; Check(!(bool)method.Invoke(null, new object[] { reply, request }), "Partial receipt acknowledged");
|
|
reply.result.reward = new StageReward { balls = 5 }; reply.result.id = "other"; Check(!(bool)method.Invoke(null, new object[] { reply, request }), "Wrong receipt acknowledged");
|
|
var gameView = Resources.FindObjectsOfTypeAll<EditorWindow>().FirstOrDefault(w => w.GetType().FullName == "UnityEditor.GameView");
|
|
if (gameView != null) SessionState.SetInt("AIGame.Mail.GameView", (int)gameView.GetType().GetProperty("selectedSizeIndex", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetValue(gameView));
|
|
return "PASS mail config, authored list/details/scroll, TMP fonts, nine-slices, complete versus malformed claim receipts.";
|
|
}
|
|
public static async Task<string> Start()
|
|
{
|
|
await Until(() => Flow?.Game != null && !Flow.IsBusy);
|
|
SessionState.SetString("AIGame.Mail.original", JsonUtility.ToJson(Flow.Game.State)); SessionState.SetString("AIGame.Mail.endpoint", Get<string>("_httpBase"));
|
|
AuthResponse account = null; string error = null; bool done = false; string user = "g1live_mail_" + Guid.NewGuid().ToString("N");
|
|
Flow.StartCoroutine(AuthApi.Register("http://127.0.0.1:18081", user, "isolated-mail-password", (r, e) => { account = r; error = e; done = true; }));
|
|
await Until(() => done); Check(account != null, "Register failed " + error);
|
|
File.WriteAllText(Path.Combine(Application.dataPath, "../../Tools/output/g1-live/mail-account.txt"), user);
|
|
var session = new SessionData { Pid = account.pid, Token = account.token, Username = user };
|
|
Set("_session", new Func<SessionData>(() => session)); Set("_profile", new Func<long>(() => session.Pid)); Set("_httpBase", "http://127.0.0.1:18081"); Set("_boundProfile", 0L);
|
|
Flow.Handle("Home"); await Until(() => !Flow.IsBusy); await Task.Delay(150);
|
|
Check(Flow.Game.State.schema >= 7 && Flow.Game.State.balls == 20 && Flow.Game.State.candy == 0, "New mail account auto-awarded wallet");
|
|
Check(GameObject.Find("Dot_Mail") != null, "Unread mail red dot missing");
|
|
Click("Mail"); await Until(() => Flow.Page == "UI_PrototypeMail"); await Capture("list");
|
|
Click("Mail0"); await Until(() => !Flow.IsBusy && Flow.Game.State.mails[0].read); Check(Flow.Game.State.balls == 20, "Read granted reward"); await Capture("detail");
|
|
Click("ClaimMail"); await Until(() => !Flow.IsBusy && Flow.Game.State.mails[0].claimed);
|
|
Check(Flow.Game.State.balls == 25 && Flow.Game.State.candy == 20 && !Button("ClaimMail").interactable, "Single claim not exact"); Click("Ok"); await Capture("claimed");
|
|
Click("Mail1"); await Until(() => !Flow.IsBusy && Flow.Game.State.mails[1].read); Check(!Button("ClaimMail").interactable, "Notification claim enabled"); await Capture("notice");
|
|
Click("Back"); await Task.Delay(150); Check(GameObject.Find("Dot_Mail") == null, "Mail dot remains after reading and claiming");
|
|
Set("_boundProfile", 0L); Flow.Handle("Home"); await Until(() => !Flow.IsBusy);
|
|
Check(Flow.Game.State.balls == 25 && Flow.Game.State.mails[0].claimed, "Reattach duplicated claim");
|
|
return "PASS actual mail entry, unread dot, read without award, exact +5 balls/+20 candy once, notice without attachment, dot cleared and state survives reattach.";
|
|
}
|
|
public static async Task<string> Diagnose()
|
|
{
|
|
var net = UnityEngine.Object.FindObjectOfType<GameNet>();
|
|
using (var request = UnityEngine.Networking.UnityWebRequest.Get(Get<string>("_httpBase") + "/game/state"))
|
|
{
|
|
request.SetRequestHeader("Authorization", "Bearer " + net.Session.Token);
|
|
var operation = request.SendWebRequest(); while (!operation.isDone) await Task.Delay(50);
|
|
var r = JsonUtility.FromJson<PrototypeReply>(request.downloadHandler.text);
|
|
var raw = Newtonsoft.Json.Linq.JObject.Parse(request.downloadHandler.text);
|
|
return "HTTP=" + request.responseCode + " schema=" + r?.state?.schema + " stateValid=" + PrototypeGame.Valid(r?.state) + " configValid=" + PrototypeConfig.Valid(r?.config) + " mailConfig=" + (r?.config?.mail != null) + " mailPresent=" + (raw["config"]?["mail"] != null) + " lifetime=" + r?.config?.mail?.lifetimeSeconds + " initial=" + r?.config?.mail?.initial?.Length + " versionMatch=" + (r?.configVersion == r?.config?.version) + " error=" + r?.error;
|
|
}
|
|
}
|
|
public static string ConsoleIssues()
|
|
{
|
|
var assembly = typeof(EditorWindow).Assembly;
|
|
var entries = assembly.GetType("UnityEditor.LogEntries"); var entryType = assembly.GetType("UnityEditor.LogEntry");
|
|
var flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
|
var itemFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
int count = (int)entries.GetMethod("StartGettingEntries", flags).Invoke(null, null);
|
|
var messages = new List<string>();
|
|
try
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var entry = Activator.CreateInstance(entryType);
|
|
entries.GetMethod("GetEntryInternal", flags).Invoke(null, new[] { (object)i, entry });
|
|
int mode = (int)entryType.GetField("mode", itemFlags).GetValue(entry);
|
|
if ((mode & (1 | 2 | 16 | 64 | 256 | 512 | 131072)) != 0)
|
|
messages.Add((string)entryType.GetField("message", itemFlags).GetValue(entry));
|
|
}
|
|
}
|
|
finally { entries.GetMethod("EndGettingEntries", flags).Invoke(null, null); }
|
|
return string.Join("\n", messages);
|
|
}
|
|
public static async Task<string> Batch()
|
|
{
|
|
await Refresh(); Check(Flow.Game.State.mails.Length == 8, "Mixed mail fixture missing"); await Capture("mixed");
|
|
Click("Mail1"); await Until(() => !Flow.IsBusy); Check(!Button("ClaimMail").interactable, "Expired claim enabled"); await Capture("expired");
|
|
Click("MailNext"); await Task.Delay(150); Check(Get<int>("_mailPage") == 1, "Next page failed");
|
|
Click("Mail0"); await Until(() => !Flow.IsBusy); await Capture("long_body");
|
|
var scroll = UnityEngine.Object.FindObjectOfType<ScrollRect>(); Canvas.ForceUpdateCanvases(); Check(scroll.content.rect.height > scroll.viewport.rect.height, "Long body cannot scroll");
|
|
scroll.verticalNormalizedPosition = 0; await Capture("long_body_bottom");
|
|
int coins = Flow.Game.State.coins, balls = Flow.Game.State.balls, candy = Flow.Game.State.candy, tickets = Flow.Game.State.tickets;
|
|
Click("ClaimAllMail"); await Until(() => !Flow.IsBusy && Get<PrototypeReply>("_reply").action == "mail.claimAll");
|
|
Check(Flow.Game.State.coins == coins + 7 && Flow.Game.State.balls == balls + 3 && Flow.Game.State.candy == candy + 20 && Flow.Game.State.tickets == tickets + 1, "Batch attachment sum wrong");
|
|
Check(Get<PrototypeReply>("_reply").result.claimedCount == 3 && !Button("ClaimAllMail").interactable, "Batch count or repeat button wrong"); await Capture("batch_result"); Click("Ok");
|
|
Click("MailPrev"); await Task.Delay(150); Check(Get<int>("_mailPage") == 0, "Previous page failed");
|
|
return "PASS paginated mixed statuses, disabled expired attachment, long-body scroll and exact three-mail batch reward with repeat disabled.";
|
|
}
|
|
public static async Task<string> LostAcknowledgement()
|
|
{
|
|
await Refresh(); var api = Get<PrototypeApi>("_api"); var mail = Flow.Game.State.mails.Single();
|
|
var command = api.Command("mail.claim", Flow.Game.State.revision, mailId: mail.mailId);
|
|
PrototypeReply response = null; string error = null; int balls = Flow.Game.State.balls;
|
|
Flow.StartCoroutine(api.Send(command, (r, e) => { response = r; error = e; })); await Until(() => response != null || error != null); Check(response?.ok == true, "First mail claim failed " + error);
|
|
var session = Get<Func<SessionData>>("_session")(); string key = "AIGame.G1.pending." + Uri.EscapeDataString("http://127.0.0.1:18081") + "." + session.Pid;
|
|
PlayerPrefs.SetString(key, JsonUtility.ToJson(command)); PlayerPrefs.Save(); Set("_needsSync", true); Flow.Handle("Mail");
|
|
await Until(() => !Flow.IsBusy && Get<PrototypeReply>("_reply").replayed);
|
|
Check(Flow.Game.State.balls == balls + 5 && !api.HasPending, "Mail replay duplicated award or retained confirmed request");
|
|
Check(Flow.Game.State.mails.Single().claimed && Flow.Game.State.mails.Single().read, "Recovered mail claim/read flags missing");
|
|
Check(UnityEngine.Object.FindObjectsOfType<TextMeshProUGUI>().Single(t => t.name == "Txt_MailStatus0").text.Contains("附件已领取"), "Recovered list status stale");
|
|
var dialog = UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().CaptureState();
|
|
Check(dialog.Visible && dialog.Title == "邮件领取结果", "Recovered mail receipt not shown"); await Capture("recovery"); Click("Ok");
|
|
return "PASS lost acknowledgement keeps original mail request, grants five once, restores receipt and clears durable queue.";
|
|
}
|
|
public static async Task<string> Empty()
|
|
{
|
|
await Refresh(); Check(Flow.Game.State.mails.Length == 0 && !Button("ClaimAllMail").interactable, "Empty mail claim enabled");
|
|
Check(GameObject.Find("Txt_MailEmpty") != null, "Missing empty mail state"); await Capture("empty");
|
|
return "PASS empty mailbox and disabled batch/attachment actions.";
|
|
}
|
|
public static async Task<string> Cleanup()
|
|
{
|
|
UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().Hide();
|
|
var testSession = Get<Func<SessionData>>("_session")();
|
|
string key = "AIGame.G1.pending." + Uri.EscapeDataString("http://127.0.0.1:18081") + "." + testSession.Pid; Check(!PlayerPrefs.HasKey(key), "Test request still pending");
|
|
var net = UnityEngine.Object.FindObjectOfType<GameNet>();
|
|
Set("_session", new Func<SessionData>(() => net.Session)); Set("_profile", new Func<long>(() => net.Session?.Pid ?? 0)); Set("_httpBase", SessionState.GetString("AIGame.Mail.endpoint", "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.Mail.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 && Flow.Game.State.balls == original.balls, "Original account resources changed during mail test");
|
|
var gameView = Resources.FindObjectsOfTypeAll<EditorWindow>().FirstOrDefault(w => w.GetType().FullName == "UnityEditor.GameView");
|
|
if (gameView != null) gameView.GetType().GetProperty("selectedSizeIndex", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(gameView, SessionState.GetInt("AIGame.Mail.GameView", 0));
|
|
return "PASS original account and Game View restored, wallet unchanged; no real claim or read operations sent.";
|
|
}
|
|
}
|