Files

260 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using AIGame.EditorTools;
using AOT.Boot;
using AOT.UI;
using HotUpdate.Gameplay;
using HotUpdate.UI;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Object = UnityEngine.Object;
public static class VerifyHomeMobileUi
{
const string MaterialPath = "Assets/Bundles/UI/Material/HomeExplorerCutout.mat";
const string SceneKey = "AIGame.HomeMobile.SceneSetup";
static string Repo => Path.GetFullPath(Path.Combine(Application.dataPath, "../.."));
static string Output => Path.Combine(Repo, "Tools/output/home_mobile");
public static string Status() => JsonConvert.SerializeObject(new {
playing = EditorApplication.isPlaying, manager = UiManager.Instance != null,
hud = UiManager.Instance != null && UiManager.Instance.Get("UI_MainHUD") != null,
bootstraps = Object.FindObjectsOfType<GameplayBootstrap>().Select(b => new { b.name, b.enabled, active = b.gameObject.activeInHierarchy }).ToArray(),
explorers = Object.FindObjectsOfType<HomeExplorerPresenter>().Select(e => new { e.name, e.Ready,
image = e.GetComponent<RawImage>()?.material?.shader?.name }).ToArray(),
backgrounds = Object.FindObjectsOfType<Image>().Where(i => i.name == "Img_Camp").Select(i => new {
sprite = i.sprite != null ? i.sprite.name : null, texture = i.sprite != null && i.sprite.texture != null ? i.sprite.texture.name : null,
textureSize = i.sprite != null && i.sprite.texture != null ? i.sprite.texture.width : 0, i.color
}).ToArray(),
roots = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects().Select(g => g.name).ToArray()
});
public static string BuildPrefab()
{
Check(!EditorApplication.isPlaying, "Stop Play Mode before authoring");
if (!AssetDatabase.IsValidFolder("Assets/Bundles/UI/Material")) AssetDatabase.CreateFolder("Assets/Bundles/UI", "Material");
var shader = AssetDatabase.LoadAssetAtPath<Shader>("Assets/HotUpdate/Shaders/HomeExplorerCutout.shader");
Check(shader != null && shader.isSupported, "Preview shader unavailable");
var material = AssetDatabase.LoadAssetAtPath<Material>(MaterialPath);
if (material == null) { material = new Material(shader); AssetDatabase.CreateAsset(material, MaterialPath); }
material.shader = shader;
EditorUtility.SetDirty(material); AssetDatabase.SaveAssets();
string json = Path.Combine(Repo, "Doc/UIPrefabCreater/UI_MainHUD.json");
var validation = JsonUIPrefabBuilder.Validate(json);
Check(validation.Ok, string.Join("\n", validation.Errors));
string path = JsonUIPrefabBuilder.BuildFromJsonFile(json);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
Check(prefab != null, "HUD build failed");
Check(prefab.transform.Find("ExplorerStage").GetComponent<RawImage>().material == material, "HUD has no explicit preview material");
Check(AssetDatabase.GetDependencies(path, true).Contains(AssetDatabase.GetAssetPath(shader)), "Shader is not a real prefab dependency");
Check(prefab.GetComponentsInChildren<UiSafeArea>(true).Length == 4, "Missing safe-area components");
Check(prefab.transform.Find("Img_Camp").GetComponent<Image>().sprite != null, "Camp background sprite is missing");
VerifyCutout(material);
return "PASS: JSON HUD, explicit material/shader dependency, four authored safe-area layouts and GPU color-key discard";
}
static void VerifyCutout(Material material)
{
var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false, true);
var target = new RenderTexture(8, 8, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
var pixels = new Texture2D(8, 8, TextureFormat.RGBA32, false, true);
var previous = RenderTexture.active;
try
{
texture.SetPixels(Enumerable.Repeat(material.GetColor("_Key"), 4).ToArray()); texture.Apply();
target.Create(); RenderTexture.active = target; GL.Clear(true, true, Color.cyan);
Graphics.Blit(texture, target, material);
RenderTexture.active = target; pixels.ReadPixels(new Rect(0, 0, 8, 8), 0, 0); pixels.Apply();
Color color = pixels.GetPixel(4, 4);
Check(color.r < .05f && color.b > .95f && color.g > .95f, "Color-key background was not discarded");
Graphics.Blit(Texture2D.whiteTexture, target, material);
RenderTexture.active = target; pixels.ReadPixels(new Rect(0, 0, 8, 8), 0, 0); pixels.Apply();
Check(pixels.GetPixel(4, 4).r > .95f, "Foreground was discarded");
}
finally { RenderTexture.active = previous; target.Release(); Object.DestroyImmediate(target); Object.DestroyImmediate(texture); Object.DestroyImmediate(pixels); }
}
public static string PrepareScene()
{
Check(!EditorApplication.isPlaying, "Stop Play Mode first");
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
Check(!UnityEngine.SceneManagement.SceneManager.GetSceneAt(i).isDirty, "Existing scene has unsaved changes");
Directory.CreateDirectory(Output);
var setup = EditorSceneManager.GetSceneManagerSetup();
string json = JsonConvert.SerializeObject(setup);
SessionState.SetString(SceneKey, json); File.WriteAllText(Path.Combine(Output, "scene_setup.json"), json);
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
SetResolution(1080, 1920, true);
return "Prepared isolated scene; original scene setup and Game View size retained";
}
public static string StartPreview()
{
Check(EditorApplication.isPlaying, "Enter Play Mode first");
var camera = GameObject.Find("HomeMobileTestCamera")?.GetComponent<Camera>();
if (camera == null) camera = new GameObject("HomeMobileTestCamera", typeof(Camera)).GetComponent<Camera>();
camera.clearFlags = CameraClearFlags.SolidColor; camera.cullingMask = 0;
camera.backgroundColor = new Color(.2f, .3f, .4f);
if (camera.GetComponent<AudioListener>() == null) camera.gameObject.AddComponent<AudioListener>();
if (EventSystem.current == null) new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
var bootstrap = Object.FindObjectOfType<GameplayBootstrap>();
if (bootstrap == null)
{
bootstrap = new GameObject("GameplayBootstrap").AddComponent<GameplayBootstrap>();
bootstrap.Bind(new BootContext { HttpBase = "http://127.0.0.1:1", UiTheme = "candy" }, null);
}
return "Isolated UI started; allow normal editor frames before checking";
}
public static string CheckReady()
{
var hud = UiManager.Instance?.Get("UI_MainHUD");
Check(hud != null && hud.GetComponentInChildren<HomeExplorerPresenter>().Ready, "UI preview is still loading");
Check(Object.FindObjectsOfType<HotUpdate.CharacterSpawner>().Length == 0,
"Startup still creates 3D world spawners");
Check(GameObject.Find("CaoCao_Runtime") == null, "Startup world was loaded");
var raw = hud.GetComponentInChildren<RawImage>();
Check(raw.enabled && raw.texture is RenderTexture && raw.material.shader.name == "AIGame/HomeExplorerCutout", "Live preview material missing");
var presenter = hud.GetComponentInChildren<HomeExplorerPresenter>();
Check(!presenter.Talking, "Explorer speaks before a tap");
return "PASS: no startup 3D world; actual serialized preview material loaded; no automatic speech";
}
public static string SetPlain() => SetScenario("plain", 1080, 1920, Vector4.zero);
public static string SetPhone() => SetScenario("iphone_notch_capsule", 1080, 2338, new Vector4(0, 34f / 844, 0, 91f / 844));
public static string SetAndroid() => SetScenario("android_capsule", 1080, 2400, new Vector4(0, 0, 0, 64f / 800));
static string SetScenario(string name, int width, int height, Vector4 insets)
{
SetResolution(width, height, false); UiSafeArea.SetPreviewInsets(insets);
SessionState.SetString(SceneKey + ".Scenario", name);
SessionState.SetInt(SceneKey + ".Width", width); SessionState.SetInt(SceneKey + ".Height", height);
SessionState.SetFloat(SceneKey + ".Top", insets.w);
return name + " selected; allow normal editor frames before capture";
}
public static string CompletePreview()
{
var hud = UiManager.Instance.Get("UI_MainHUD");
var presenter = hud.GetComponentInChildren<HomeExplorerPresenter>();
hud.transform.Find("Btn_Explorer").GetComponent<Button>().onClick.Invoke();
Check(presenter.Talking, "Explorer speech no longer works");
foreach (string name in new[] { "plain", "iphone_notch_capsule", "android_capsule" })
Check(File.Exists(Path.Combine(Output, name + ".png")), "Missing screenshot: " + name);
Save("runtime", new { passed = true, noStartupWorld = true, materialDependency = true,
scenarios = new[] { "plain", "iphone_notch_capsule", "android_capsule" }, explorerMovesAndTalks = true });
return "PASS: actual GameplayBootstrap has no startup world; explorer preview and speech; three screen/safe-area scenarios and header hit targets";
}
public static string CapturePreview()
{
var hud = UiManager.Instance.Get("UI_MainHUD");
string name = SessionState.GetString(SceneKey + ".Scenario", "");
int width = SessionState.GetInt(SceneKey + ".Width", 0), height = SessionState.GetInt(SceneKey + ".Height", 0);
Check(Screen.width == width && Screen.height == height, "Game View has not resized yet");
Canvas.ForceUpdateCanvases();
float upper = Screen.height * (1 - SessionState.GetFloat(SceneKey + ".Top", 0));
foreach (string node in new[] { "Layout_Top", "Panel_Top" })
{
var rect = (RectTransform)hud.transform.Find(node);
var corners = new Vector3[4]; rect.GetWorldCorners(corners);
Check(corners.Max(c => c.y) <= upper + 1, "Top layout overlaps native area: " + node);
}
var background = (RectTransform)hud.transform.Find("Img_Camp");
Check(background.anchorMin == Vector2.zero && background.anchorMax == Vector2.one &&
background.offsetMin == Vector2.zero && background.offsetMax == Vector2.zero, "Background no longer fills screen");
var top = hud.transform.Find("Layout_Top");
foreach (var button in top.GetComponentsInChildren<Button>())
{
var rect = (RectTransform)button.transform;
var data = new PointerEventData(EventSystem.current) { position = rect.TransformPoint(rect.rect.center) };
var hits = new List<RaycastResult>(); EventSystem.current.RaycastAll(data, hits);
Check(hits.Count > 0 && ExecuteEvents.GetEventHandler<IPointerClickHandler>(hits[0].gameObject) == button.gameObject,
"Header hit target blocked: " + button.name);
}
// Render an authored Canvas with a camera; avoid synchronous screen-backbuffer capture.
var camera = GameObject.Find("HomeMobileTestCamera").GetComponent<Camera>();
var canvas = hud.GetComponent<Canvas>(); var mode = canvas.renderMode; var worldCamera = canvas.worldCamera;
int mask = camera.cullingMask; var previous = RenderTexture.active;
var target = new RenderTexture(width, height, 24); var pixels = new Texture2D(width, height, TextureFormat.RGB24, false);
try
{
camera.targetTexture = target; camera.cullingMask = 1 << LayerMask.NameToLayer("UI");
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = 1;
Canvas.ForceUpdateCanvases(); camera.Render();
RenderTexture.active = target; pixels.ReadPixels(new Rect(0, 0, width, height), 0, 0); pixels.Apply();
Directory.CreateDirectory(Output); File.WriteAllBytes(Path.Combine(Output, name + ".png"), pixels.EncodeToPNG());
}
finally
{
RenderTexture.active = previous; camera.targetTexture = null; camera.cullingMask = mask;
canvas.renderMode = mode; canvas.worldCamera = worldCamera; Canvas.ForceUpdateCanvases();
target.Release(); Object.Destroy(target); Object.Destroy(pixels);
}
return "PASS " + name + ": header bounds/hit targets and full-screen background; image=" + Path.Combine(Output, name + ".png");
}
static void SetResolution(int width, int height, bool remember)
{
var editor = typeof(Editor).Assembly; var viewType = editor.GetType("UnityEditor.GameView");
var window = EditorWindow.GetWindow(viewType);
var selected = viewType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (remember) SessionState.SetInt(SceneKey + ".Size", (int)selected.GetValue(window));
var sizesType = editor.GetType("UnityEditor.GameViewSizes");
var sizes = typeof(ScriptableSingleton<>).MakeGenericType(sizesType).GetProperty("instance").GetValue(null);
var group = sizesType.GetMethod("GetGroup").Invoke(sizes, new[] { sizesType.GetProperty("currentGroupType").GetValue(sizes) });
int count = (int)group.GetType().GetMethod("GetTotalCount").Invoke(group, null); int index = count;
for (int i = 0; i < count; i++)
{
var size = group.GetType().GetMethod("GetGameViewSize").Invoke(group, new object[] { i });
if ((int)size.GetType().GetProperty("width").GetValue(size) == width && (int)size.GetType().GetProperty("height").GetValue(size) == height) { index = i; break; }
}
if (index == count)
{
var size = Activator.CreateInstance(editor.GetType("UnityEditor.GameViewSize"), new object[] {
Enum.ToObject(editor.GetType("UnityEditor.GameViewSizeType"), 1), width, height, "Home mobile " + width + "x" + height });
group.GetType().GetMethod("AddCustomSize").Invoke(group, new[] { size });
}
selected.SetValue(window, index); window.Repaint();
}
public static string RestoreScene()
{
Check(!EditorApplication.isPlaying, "Stop Play Mode first");
string json = SessionState.GetString(SceneKey, "");
if (string.IsNullOrEmpty(json) && File.Exists(Path.Combine(Output, "scene_setup.json")))
json = File.ReadAllText(Path.Combine(Output, "scene_setup.json"));
Check(!string.IsNullOrEmpty(json), "Missing saved scene setup");
EditorSceneManager.RestoreSceneManagerSetup(JsonConvert.DeserializeObject<SceneSetup[]>(json));
var type = typeof(Editor).Assembly.GetType("UnityEditor.GameView"); var window = EditorWindow.GetWindow(type);
type.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.SetValue(window, SessionState.GetInt(SceneKey + ".Size", 0)); window.Repaint();
UiSafeArea.SetPreviewInsets(null); SessionState.EraseString(SceneKey);
return "Restored original scenes and Game View";
}
public static string StageResources()
{
Check(!EditorApplication.isPlaying, "Stop Play Mode before building resources");
string art = AOT.Editor.YooAssetTools.BuildAndStageArt.BuildAndStage();
Check(art.StartsWith("ok", StringComparison.Ordinal), art);
string raw = AOT.Editor.YooAssetTools.BuildAndStageHotUpdate.BuildAndStage();
Check(raw.StartsWith("ok", StringComparison.Ordinal), raw);
HybridCLR.Editor.Commands.LinkGeneratorCommand.GenerateLinkXml(EditorUserBuildSettings.activeBuildTarget);
Save("staged", new { passed = true, art, raw, hybridClrLinkUpdated = true, remotePublished = false });
return art + " | " + raw;
}
static void Save(string name, object value)
{
Directory.CreateDirectory(Output); File.WriteAllText(Path.Combine(Output, name + ".json"), JsonConvert.SerializeObject(value, Formatting.Indented));
}
static void Check(bool condition, string message) { if (!condition) throw new Exception(message); }
}