260 lines
16 KiB
C#
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); }
|
|
}
|