Files

274 lines
17 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using AIGame.EditorTools;
using HotUpdate.UI;
using Newtonsoft.Json.Linq;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Reflection;
using System.Threading.Tasks;
/// <summary>Pipeline-only import, JSON-to-Prefab build, isolated rendering and binding checks.</summary>
public static class BuildHomeUi
{
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
const string Folder = "Assets/Bundles/UI/Texture/Home";
static readonly string[] Icons = { "coin", "diamond", "energy", "avatar", "tasks", "gift", "mail", "trophy", "bag", "home", "battle", "pets", "chest", "calendar", "settings", "pinball" };
static readonly string[] Names = { "UI_MainHUD" };
static string Design => Path.Combine(Repo, "Doc/Design/UI/Home");
public static string Build()
{
if (EditorApplication.isPlaying) throw new Exception("Build in Edit Mode.");
Directory.CreateDirectory(Folder);
File.Copy(Path.Combine(Design, "home_camp_source.png"), Folder + "/home_camp.png", true);
var atlas = new Texture2D(2, 2, TextureFormat.RGBA32, false);
try
{
if (!atlas.LoadImage(File.ReadAllBytes(Path.Combine(Design, "home_icons_source.png")))) throw new Exception("Invalid icon sheet.");
for (int i = 0; i < Icons.Length; i++)
{
int col = i % 4, row = 3 - i / 4;
int x = Mathf.RoundToInt(col * atlas.width / 4f), y = Mathf.RoundToInt(row * atlas.height / 4f);
int w = Mathf.RoundToInt((col + 1) * atlas.width / 4f) - x;
int h = Mathf.RoundToInt((row + 1) * atlas.height / 4f) - y;
var icon = new Texture2D(w, h, TextureFormat.RGBA32, false);
try { icon.SetPixels(CleanSlice(atlas.GetPixels(x, y, w, h), w, h)); icon.Apply(); File.WriteAllBytes(Folder + "/home_ico_" + Icons[i] + ".png", icon.EncodeToPNG()); }
finally { UnityEngine.Object.DestroyImmediate(icon); }
}
}
finally { UnityEngine.Object.DestroyImmediate(atlas); }
AssetDatabase.Refresh();
foreach (var path in Directory.GetFiles(Folder, "*.png"))
{
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Single;
importer.spritePixelsPerUnit = 100;
var settings = new TextureImporterSettings(); importer.ReadTextureSettings(settings);
settings.spriteMeshType = SpriteMeshType.FullRect; importer.SetTextureSettings(settings);
importer.alphaIsTransparency = true; importer.mipmapEnabled = false;
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.filterMode = FilterMode.Bilinear; importer.wrapMode = TextureWrapMode.Clamp;
importer.maxTextureSize = 2048; importer.SaveAndReimport();
}
foreach (var name in Names)
{
var path = Path.Combine(Repo, "Doc/UIPrefabCreater", name + ".json");
var validation = JsonUIPrefabBuilder.Validate(path);
if (!validation.Ok) throw new Exception(string.Join("\n", validation.Errors));
if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(path))) throw new Exception("Build failed: " + name);
}
return "Imported 17 Home sprites; built Candy home HUD prefab from JSON.";
}
// Atlas extraction: discard isolated fragments spilling in from adjacent cells.
// This preserves original icon pixels and alpha; it does not repaint the source artwork.
static Color[] CleanSlice(Color[] pixels, int width, int height)
{
var visited = new bool[pixels.Length];
var components = new List<List<int>>();
for (int start = 0; start < pixels.Length; start++)
{
if (visited[start] || pixels[start].a < 0.01f) continue;
var queue = new Queue<int>(); var component = new List<int>(); queue.Enqueue(start); visited[start] = true;
while (queue.Count > 0)
{
int p = queue.Dequeue(); component.Add(p);
int px = p % width, py = p / width;
for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++)
{
int nx = px + dx, ny = py + dy;
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
int next = ny * width + nx;
if (visited[next] || pixels[next].a < 0.01f) continue;
visited[next] = true; queue.Enqueue(next);
}
}
components.Add(component);
}
int largest = components.Count > 0 ? components.Max(c => c.Count) : 0;
foreach (var c in components.Where(c => c.Count < largest * 0.03f))
foreach (int p in c) pixels[p] = Color.clear;
return pixels;
}
public static string Verify()
{
int checks = 0;
Action<bool, string> check = (ok, message) => { if (!ok) throw new Exception(message); checks++; };
foreach (var name in Names)
{
var doc = JObject.Parse(File.ReadAllText(Path.Combine(Repo, "Doc/UIPrefabCreater", name + ".json")));
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/" + name + ".prefab");
check(prefab != null, "Missing prefab " + name);
var scene = EditorSceneManager.NewPreviewScene();
var go = UnityEngine.Object.Instantiate(prefab); SceneManager.MoveGameObjectToScene(go, scene);
try
{
foreach (var path in ((JObject)doc["assets"]["sprites"]).Properties())
check(AssetDatabase.LoadAssetAtPath<Sprite>((string)path.Value) != null, "Missing sprite " + path.Name);
var images = go.GetComponentsInChildren<Image>(true);
check(images.All(i => i.sprite != null), "Unresolved prefab image " + name);
check(go.GetComponentsInChildren<CanvasScaler>().Single().referenceResolution == new Vector2(1080, 1920), "Wrong resolution");
foreach (var b in doc["bindings"])
check(go.GetComponentsInChildren<Transform>(true).Any(t => t.name == (string)b), "Missing binding " + b);
check(!go.GetComponentsInChildren<Transform>(true).Any(t => t.name == "UI_ThemeSwitcher"), "Unexpected theme toolbar");
var view = go.AddComponent<MainHudView>(); view.Bind();
string called = null; int invocations = 0, reconnect = 0;
view.OnHomeActionClicked += k => { called = k; invocations++; };
view.OnReconnectClicked += () => reconnect++;
view.Bind(); // Bind is safe to repeat.
foreach (var e in doc["events"])
{
var button = go.GetComponentsInChildren<Button>(true).Single(b => b.name == (string)e["node"]);
check(button.GetComponent<RectTransform>().sizeDelta.x >= 88 && button.GetComponent<RectTransform>().sizeDelta.y >= 82, "Small touch target " + button.name);
button.onClick.Invoke();
var eventName = (string)e["name"];
if (eventName != "MainHUD.Reconnect")
check(called == eventName.Substring("MainHUD.".Length), "Wrong click callback: " + eventName);
}
check(invocations == 18 && reconnect == 1, "Duplicate or missing event callbacks");
view.SetStatus("已连接"); view.SetPid(10248); view.ShowReconnect(true, "网络已断开,请重试");
check(go.transform.Find("Panel_Reconnect").gameObject.activeSelf, "Reconnect not visible");
check(go.GetComponentsInChildren<TextMeshProUGUI>(true).Single(t => t.name == "Txt_Reconnect").text == "网络已断开,请重试", "Reconnect message missing");
check(go.GetComponentsInChildren<TextMeshProUGUI>(true).Single(t => t.name == "Txt_Status").text == "已连接", "Status binding broken");
view.ShowReconnect(false); check(!go.transform.Find("Panel_Reconnect").gameObject.activeSelf, "Reconnect not hidden");
}
finally { UnityEngine.Object.DestroyImmediate(go); EditorSceneManager.ClosePreviewScene(scene); }
}
foreach (var path in Directory.GetFiles(Folder, "*.png"))
{
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
check(importer.textureType == TextureImporterType.Sprite && !importer.mipmapEnabled && importer.textureCompression == TextureImporterCompression.Uncompressed, "Wrong sprite settings " + path);
}
return checks + " sprite / schema / bindings / touch-target / click / reconnect checks passed.";
}
public static string Preview()
{
foreach (var name in Names) Render(name, 1080, 1920, false);
Render("UI_MainHUD", 1080, 2400, false);
Render("UI_MainHUD", 1080, 1920, true);
return "Rendered Candy, tall-screen and reconnect previews: " + Design;
}
public static string PrepareLive()
{
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);
SessionState.SetInt("AIGame.Home.PreviousGameViewSize", (int)selected.GetValue(window));
var sizesType = editor.GetType("UnityEditor.GameViewSizes");
var sizes = typeof(ScriptableSingleton<>).MakeGenericType(sizesType).GetProperty("instance").GetValue(null);
var getGroup = sizesType.GetMethod("GetGroup");
var groupType = sizesType.GetProperty("currentGroupType").GetValue(sizes);
var group = getGroup.Invoke(sizes, new[] { groupType });
int count = (int)group.GetType().GetMethod("GetTotalCount").Invoke(group, null);
int found = -1;
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) == 1080 && (int)size.GetType().GetProperty("height").GetValue(size) == 1920) { found = i; break; }
}
if (found < 0)
{
var sizeType = editor.GetType("UnityEditor.GameViewSize"); var kind = editor.GetType("UnityEditor.GameViewSizeType");
var size = Activator.CreateInstance(sizeType, new object[] { Enum.ToObject(kind, 1), 1080, 1920, "AIGame Home 1080x1920" });
group.GetType().GetMethod("AddCustomSize").Invoke(group, new[] { size }); found = count;
}
selected.SetValue(window, found); window.Repaint();
return "Game View set to portrait 1080x1920 for live UI validation.";
}
public static string RestoreGameView()
{
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("AIGame.Home.PreviousGameViewSize", 0));
window.Repaint(); return "Restored previous Game View size.";
}
static async Task Until(Func<bool> condition)
{
var start = DateTime.UtcNow;
while (!condition()) { if ((DateTime.UtcNow - start).TotalSeconds > 20) throw new Exception("Live HUD did not become ready."); await Task.Delay(100); }
}
static void Click(Button button)
{
Canvas.ForceUpdateCanvases();
var rect = button.GetComponent<RectTransform>();
var 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);
if (hits.Count == 0 || ExecuteEvents.GetEventHandler<IPointerClickHandler>(hits[0].gameObject) != button.gameObject) throw new Exception("Button raycast blocked: " + button.name + "; hits=" + string.Join(",", hits.Take(4).Select(h => h.gameObject.name)));
ExecuteEvents.Execute(button.gameObject, data, ExecuteEvents.pointerClickHandler);
}
public static async Task<string> VerifyLive()
{
if (!EditorApplication.isPlaying) throw new Exception("Enter Play Mode first.");
await Until(() => UiManager.Instance != null && UiManager.Instance.Get(ConfirmDialogView.PrefabName) != null);
var hud = UiManager.Instance.Get(MainHudView.PrefabName);
var dialog = UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>();
if (EventSystem.current == null) throw new Exception("No EventSystem.");
var flow = UnityEngine.Object.FindObjectOfType<HotUpdate.Gameplay.PrototypeFlow>();
await Until(() => flow?.Game != null && !flow.IsBusy);
dialog.Hide();
flow.Handle("Home");
await Task.Delay(100);
var entries = new[] { ("Btn_Adventure", "Stages"), ("Btn_Tasks", "Tasks"), ("Btn_Mail", "Mail"), ("Btn_Rewards", "Chest"), ("Btn_NavPets", "Pets"), ("Btn_NavShop", "Shop"), ("Btn_NavBattle", "Stages"), ("Btn_NavPinball", "Pinball"), ("Btn_Settings", "") };
foreach (var entry in entries)
{
Click(hud.GetComponentsInChildren<Button>().Single(b => b.name == entry.Item1));
if (entry.Item2.Length > 0) { await Until(() => flow.Page == "UI_Prototype" + entry.Item2); flow.Handle("Home"); await Until(() => flow.Page == "Home"); }
else { await Until(() => dialog.gameObject.activeSelf); await Task.Delay(100); Click(dialog.GetComponentsInChildren<Button>().Single(b => b.name == "Btn_Ok")); if (dialog.gameObject.activeSelf) throw new Exception("Dialog did not close."); }
await Task.Delay(150);
}
Click(hud.GetComponentsInChildren<Button>().Single(b => b.name == "Btn_NavHome"));
await Until(() => flow.Page == "UI_PrototypeCamp"); flow.Handle("Home");
return "9 home feature entries and camp passed live EventSystem raycasts, authored pages and remaining support dialogs; screen=" + Screen.width + "x" + Screen.height + "; theme=" + UiManager.Instance.Theme;
}
static void Render(string name, int width, int height, bool reconnect)
{
var scene = EditorSceneManager.NewPreviewScene();
var previous = RenderTexture.active;
var rt = new RenderTexture(width, height, 24);
try
{
var cameraGo = new GameObject("HomePreviewCamera", typeof(Camera)); SceneManager.MoveGameObjectToScene(cameraGo, scene);
var camera = cameraGo.GetComponent<Camera>(); camera.scene = scene;
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.92f, 0.95f, 0.97f);
camera.orthographic = true; camera.orthographicSize = height / 2f; camera.transform.position = new Vector3(0, 0, -100);
camera.targetTexture = rt;
var go = UnityEngine.Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/" + name + ".prefab"));
SceneManager.MoveGameObjectToScene(go, scene);
var canvas = go.GetComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = 10;
var scaler = go.GetComponent<CanvasScaler>(); scaler.enabled = false; canvas.scaleFactor = 1;
var view = go.AddComponent<MainHudView>(); view.Bind(); view.SetStatus("欢迎回到小镇");
if (reconnect) view.ShowReconnect(true, "网络连接已断开,正在为你恢复连接…");
Canvas.ForceUpdateCanvases();
foreach (var text in go.GetComponentsInChildren<TextMeshProUGUI>(true)) text.ForceMeshUpdate(true);
Canvas.ForceUpdateCanvases(); camera.Render();
RenderTexture.active = rt;
var screenshot = new Texture2D(width, height, TextureFormat.RGB24, false);
try
{
screenshot.ReadPixels(new Rect(0, 0, width, height), 0, 0); screenshot.Apply();
File.WriteAllBytes(Path.Combine(Design, name + (reconnect ? "_reconnect" : height == 2400 ? "_tall" : "") + ".png"), screenshot.EncodeToPNG());
}
finally { UnityEngine.Object.DestroyImmediate(screenshot); }
}
finally { RenderTexture.active = previous; EditorSceneManager.ClosePreviewScene(scene); rt.Release(); UnityEngine.Object.DestroyImmediate(rt); }
}
}