461 lines
20 KiB
C#
461 lines
20 KiB
C#
#if UNITY_EDITOR
|
|
using System;
|
|
using System.Linq;
|
|
using AibisDream.UI;
|
|
using TMPro;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace AibisDream.DeveloperMode.Editor
|
|
{
|
|
/// <summary>只重建 DeveloperModePanel 的测试存档页,避免覆盖其他页的人工调整。</summary>
|
|
public static class DeveloperModeTestSavePanelBuilder
|
|
{
|
|
private const string PrefabPath =
|
|
"Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab";
|
|
private const string FontAssetPath =
|
|
"Assets/Font/Assets/WenQuanYi Bitmap Song 14px SDF.asset";
|
|
|
|
private static readonly Color Surface = new(0.055f, 0.085f, 0.11f, 0.96f);
|
|
private static readonly Color Accent = new(0.16f, 0.75f, 0.88f, 1f);
|
|
private static readonly Color TextColor = new(0.88f, 0.96f, 1f, 1f);
|
|
|
|
[MenuItem("Tools/AIBIS/Developer Mode/Rebuild Test Save Page")]
|
|
public static void BuildFromMenu()
|
|
{
|
|
Build();
|
|
Selection.activeObject = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
|
|
}
|
|
|
|
public static void BuildFromCommandLine()
|
|
{
|
|
try
|
|
{
|
|
Build();
|
|
Debug.Log("[DeveloperModeTestSavePanelBuilder] Build completed.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogException(ex);
|
|
EditorApplication.Exit(1);
|
|
}
|
|
}
|
|
|
|
private static void Build()
|
|
{
|
|
var root = PrefabUtility.LoadPrefabContents(PrefabPath);
|
|
try
|
|
{
|
|
var panel = root.GetComponent<DeveloperModePanel>()
|
|
?? throw new InvalidOperationException("DeveloperModePanel component not found.");
|
|
var serialized = new SerializedObject(panel);
|
|
var pages = serialized.FindProperty("tabPages")
|
|
?? throw new MissingFieldException(nameof(DeveloperModePanel), "tabPages");
|
|
if (pages.arraySize < 4)
|
|
{
|
|
throw new InvalidOperationException("DeveloperModePanel requires five tab pages.");
|
|
}
|
|
|
|
var savePage = pages.GetArrayElementAtIndex(3).objectReferenceValue as GameObject
|
|
?? throw new InvalidOperationException("Save page reference is missing.");
|
|
ClearChildren(savePage.transform);
|
|
ConfigurePageLayout(savePage);
|
|
BuildSavePage(savePage.transform, serialized);
|
|
RenameSaveTab(serialized);
|
|
|
|
var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(FontAssetPath)
|
|
?? throw new InvalidOperationException($"Font not found: {FontAssetPath}");
|
|
foreach (var text in root.GetComponentsInChildren<TMP_Text>(true))
|
|
{
|
|
text.font = font;
|
|
}
|
|
|
|
serialized.ApplyModifiedPropertiesWithoutUndo();
|
|
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
|
|
}
|
|
finally
|
|
{
|
|
PrefabUtility.UnloadPrefabContents(root);
|
|
}
|
|
|
|
AssetDatabase.SaveAssets();
|
|
}
|
|
|
|
private static void BuildSavePage(Transform page, SerializedObject serialized)
|
|
{
|
|
var recording = CreateHorizontal("Recording Toolbar", page, 6f);
|
|
AddLayout(recording, 38f);
|
|
var recordingToggle = CreateToggle("Record Test Saves", recording.transform, "录制测试存档", 145f);
|
|
var recordingState = CreateText("Recording State", recording.transform, "○ OFF", 14f, FontStyles.Bold);
|
|
AddLayout(recordingState.gameObject, 34f);
|
|
recordingState.rectTransform.sizeDelta = new Vector2(105f, 0f);
|
|
var search = CreateInput("Test Save Search", recording.transform, "搜索章节、节点或场景...", 250f);
|
|
var reload = CreateButton("Reload Test Saves", recording.transform, "重新扫描", 92f, 34f);
|
|
var open = CreateButton("Open Test Saves", recording.transform, "打开目录", 92f, 34f);
|
|
|
|
var columns = CreateHorizontal("Library Columns", page, 7f);
|
|
var columnsLayout = columns.AddComponent<LayoutElement>();
|
|
columnsLayout.flexibleHeight = 1f;
|
|
columnsLayout.minHeight = 420f;
|
|
|
|
var chapters = CreateVertical("Chapter Column", columns.transform, 4f);
|
|
var chapterLayout = chapters.AddComponent<LayoutElement>();
|
|
chapterLayout.preferredWidth = 245f;
|
|
chapterLayout.flexibleWidth = 0f;
|
|
chapterLayout.flexibleHeight = 1f;
|
|
var chapterTitle = CreateText("Title", chapters.transform, "章节 / 覆盖率", 16f, FontStyles.Bold);
|
|
AddLayout(chapterTitle.gameObject, 28f);
|
|
CreateList(
|
|
"Chapter List",
|
|
chapters.transform,
|
|
out var chapterContent,
|
|
out var chapterTemplate);
|
|
|
|
var entries = CreateVertical("Entry Column", columns.transform, 4f);
|
|
var entryLayout = entries.AddComponent<LayoutElement>();
|
|
entryLayout.flexibleWidth = 1f;
|
|
entryLayout.flexibleHeight = 1f;
|
|
var entryTitle = CreateText(
|
|
"Title",
|
|
entries.transform,
|
|
"存档点(● 已录制 / ○ 缺失 / ✕ 无效)",
|
|
16f,
|
|
FontStyles.Bold);
|
|
AddLayout(entryTitle.gameObject, 28f);
|
|
CreateList(
|
|
"Entry List",
|
|
entries.transform,
|
|
out var entryContent,
|
|
out var entryTemplate);
|
|
|
|
var actions = CreateHorizontal("Actions", page, 6f);
|
|
AddLayout(actions, 36f);
|
|
var jump = CreateButton("Jump", actions.transform, "跳转到选中", 118f, 32f);
|
|
var deleteSelected = CreateButton("Delete Selected", actions.transform, "删除选中", 105f, 32f);
|
|
var deleteInvalid = CreateButton("Delete Invalid", actions.transform, "清理无效", 105f, 32f);
|
|
var clear = CreateButton("Clear Library", actions.transform, "清空存档库", 118f, 32f);
|
|
|
|
var status = CreateText(
|
|
"Save Status",
|
|
page,
|
|
"测试存档录制默认关闭;开启后仅复制正式节点自动档。",
|
|
14f);
|
|
AddLayout(status.gameObject, 46f);
|
|
status.color = new Color(0.45f, 1f, 0.65f);
|
|
status.alignment = TextAlignmentOptions.TopLeft;
|
|
|
|
Set(serialized, "testSaveRecordingToggle", recordingToggle);
|
|
Set(serialized, "testSaveRecordingStateText", recordingState);
|
|
Set(serialized, "testSaveSearchInput", search);
|
|
Set(serialized, "reloadSavesButton", reload.GetComponent<Button>());
|
|
Set(serialized, "openTestSaveFolderButton", open.GetComponent<Button>());
|
|
Set(serialized, "jumpToTestSaveButton", jump.GetComponent<Button>());
|
|
Set(serialized, "deleteSelectedTestSaveButton", deleteSelected.GetComponent<Button>());
|
|
Set(serialized, "deleteInvalidTestSavesButton", deleteInvalid.GetComponent<Button>());
|
|
Set(serialized, "clearTestSaveLibraryButton", clear.GetComponent<Button>());
|
|
Set(serialized, "saveStatusText", status);
|
|
Set(serialized, "testSaveChapterContent", chapterContent);
|
|
Set(serialized, "testSaveChapterRowTemplate", chapterTemplate);
|
|
Set(serialized, "testSaveEntryContent", entryContent);
|
|
Set(serialized, "testSaveEntryRowTemplate", entryTemplate);
|
|
}
|
|
|
|
private static void RenameSaveTab(SerializedObject serialized)
|
|
{
|
|
var buttons = serialized.FindProperty("tabButtons");
|
|
if (buttons == null || buttons.arraySize < 4) return;
|
|
var button = buttons.GetArrayElementAtIndex(3).objectReferenceValue as Button;
|
|
var label = button?.GetComponentInChildren<TMP_Text>(true);
|
|
if (label != null) label.text = "测试存档";
|
|
}
|
|
|
|
private static void ConfigurePageLayout(GameObject page)
|
|
{
|
|
var layout = page.GetComponent<VerticalLayoutGroup>() ?? page.AddComponent<VerticalLayoutGroup>();
|
|
layout.padding = new RectOffset(8, 8, 8, 8);
|
|
layout.spacing = 6f;
|
|
layout.childAlignment = TextAnchor.UpperLeft;
|
|
layout.childControlWidth = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandHeight = false;
|
|
}
|
|
|
|
private static void ClearChildren(Transform parent)
|
|
{
|
|
for (var index = parent.childCount - 1; index >= 0; index--)
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(parent.GetChild(index).gameObject);
|
|
}
|
|
}
|
|
|
|
private static GameObject CreateUiObject(string name, params Type[] components)
|
|
{
|
|
var types = components.Contains(typeof(RectTransform))
|
|
? components
|
|
: new[] { typeof(RectTransform) }.Concat(components).ToArray();
|
|
var result = new GameObject(name, types);
|
|
result.layer = LayerMask.NameToLayer("UI");
|
|
return result;
|
|
}
|
|
|
|
private static GameObject CreatePanel(string name, Transform parent, Color color)
|
|
{
|
|
var result = CreateUiObject(name, typeof(CanvasRenderer), typeof(Image));
|
|
result.transform.SetParent(parent, false);
|
|
result.GetComponent<Image>().color = color;
|
|
return result;
|
|
}
|
|
|
|
private static GameObject CreateVertical(string name, Transform parent, float spacing)
|
|
{
|
|
var result = CreateUiObject(name, typeof(VerticalLayoutGroup));
|
|
result.transform.SetParent(parent, false);
|
|
var layout = result.GetComponent<VerticalLayoutGroup>();
|
|
layout.spacing = spacing;
|
|
layout.childAlignment = TextAnchor.UpperLeft;
|
|
layout.childControlWidth = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandHeight = false;
|
|
return result;
|
|
}
|
|
|
|
private static GameObject CreateHorizontal(string name, Transform parent, float spacing)
|
|
{
|
|
var result = CreateUiObject(name, typeof(HorizontalLayoutGroup));
|
|
result.transform.SetParent(parent, false);
|
|
var layout = result.GetComponent<HorizontalLayoutGroup>();
|
|
layout.spacing = spacing;
|
|
layout.childAlignment = TextAnchor.MiddleLeft;
|
|
layout.childControlWidth = true;
|
|
layout.childControlHeight = true;
|
|
layout.childForceExpandWidth = false;
|
|
layout.childForceExpandHeight = true;
|
|
return result;
|
|
}
|
|
|
|
private static TMP_Text CreateText(
|
|
string name,
|
|
Transform parent,
|
|
string value,
|
|
float size,
|
|
FontStyles style = FontStyles.Normal)
|
|
{
|
|
var result = CreateUiObject(name, typeof(CanvasRenderer), typeof(TextMeshProUGUI));
|
|
result.transform.SetParent(parent, false);
|
|
var text = result.GetComponent<TextMeshProUGUI>();
|
|
text.font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(FontAssetPath);
|
|
text.text = value;
|
|
text.fontSize = size;
|
|
text.fontStyle = style;
|
|
text.color = TextColor;
|
|
text.enableWordWrapping = true;
|
|
text.overflowMode = TextOverflowModes.Ellipsis;
|
|
text.raycastTarget = false;
|
|
return text;
|
|
}
|
|
|
|
private static GameObject CreateButton(
|
|
string name,
|
|
Transform parent,
|
|
string label,
|
|
float width,
|
|
float height)
|
|
{
|
|
var result = CreateUiObject(
|
|
name,
|
|
typeof(CanvasRenderer),
|
|
typeof(Image),
|
|
typeof(Button),
|
|
typeof(LayoutElement));
|
|
result.transform.SetParent(parent, false);
|
|
var image = result.GetComponent<Image>();
|
|
image.color = new Color(0.09f, 0.18f, 0.23f, 1f);
|
|
var button = result.GetComponent<Button>();
|
|
button.targetGraphic = image;
|
|
var layout = result.GetComponent<LayoutElement>();
|
|
layout.preferredWidth = width;
|
|
layout.preferredHeight = height;
|
|
var text = CreateText("Label", result.transform, label, 14f, FontStyles.Bold);
|
|
Stretch(text.rectTransform, new Vector2(5f, 2f), new Vector2(-5f, -2f));
|
|
text.alignment = TextAlignmentOptions.Center;
|
|
return result;
|
|
}
|
|
|
|
private static TMP_InputField CreateInput(
|
|
string name,
|
|
Transform parent,
|
|
string placeholder,
|
|
float width)
|
|
{
|
|
var root = CreateUiObject(
|
|
name,
|
|
typeof(CanvasRenderer),
|
|
typeof(Image),
|
|
typeof(TMP_InputField),
|
|
typeof(LayoutElement));
|
|
root.transform.SetParent(parent, false);
|
|
root.GetComponent<Image>().color = new Color(0.035f, 0.065f, 0.085f, 1f);
|
|
var layout = root.GetComponent<LayoutElement>();
|
|
layout.preferredWidth = width;
|
|
layout.preferredHeight = 34f;
|
|
var viewport = CreateUiObject("Text Area", typeof(RectMask2D));
|
|
viewport.transform.SetParent(root.transform, false);
|
|
Stretch(viewport.GetComponent<RectTransform>(), new Vector2(8f, 3f), new Vector2(-8f, -3f));
|
|
var placeholderText = CreateText("Placeholder", viewport.transform, placeholder, 14f);
|
|
Stretch(placeholderText.rectTransform);
|
|
placeholderText.color = new Color(0.45f, 0.58f, 0.63f, 1f);
|
|
placeholderText.fontStyle = FontStyles.Italic;
|
|
placeholderText.alignment = TextAlignmentOptions.MidlineLeft;
|
|
var inputText = CreateText("Text", viewport.transform, string.Empty, 14f);
|
|
Stretch(inputText.rectTransform);
|
|
inputText.alignment = TextAlignmentOptions.MidlineLeft;
|
|
var input = root.GetComponent<TMP_InputField>();
|
|
input.textViewport = viewport.GetComponent<RectTransform>();
|
|
input.textComponent = inputText;
|
|
input.placeholder = placeholderText;
|
|
input.lineType = TMP_InputField.LineType.SingleLine;
|
|
return input;
|
|
}
|
|
|
|
private static Toggle CreateToggle(string name, Transform parent, string label, float width)
|
|
{
|
|
var root = CreateUiObject(name, typeof(Toggle), typeof(LayoutElement));
|
|
root.transform.SetParent(parent, false);
|
|
var layout = root.GetComponent<LayoutElement>();
|
|
layout.preferredWidth = width;
|
|
layout.preferredHeight = 34f;
|
|
var box = CreatePanel("Background", root.transform, new Color(0.08f, 0.15f, 0.19f, 1f));
|
|
var boxRect = box.GetComponent<RectTransform>();
|
|
boxRect.anchorMin = boxRect.anchorMax = new Vector2(0f, 0.5f);
|
|
boxRect.pivot = new Vector2(0f, 0.5f);
|
|
boxRect.sizeDelta = new Vector2(24f, 24f);
|
|
var check = CreatePanel("Checkmark", box.transform, Accent);
|
|
Stretch(check.GetComponent<RectTransform>(), new Vector2(5f, 5f), new Vector2(-5f, -5f));
|
|
var text = CreateText("Label", root.transform, label, 15f);
|
|
SetAnchors(text.rectTransform, Vector2.zero, Vector2.one, new Vector2(34f, 0f), Vector2.zero);
|
|
text.alignment = TextAlignmentOptions.MidlineLeft;
|
|
var toggle = root.GetComponent<Toggle>();
|
|
toggle.targetGraphic = box.GetComponent<Image>();
|
|
toggle.graphic = check.GetComponent<Image>();
|
|
toggle.isOn = false;
|
|
return toggle;
|
|
}
|
|
|
|
private static void CreateList(
|
|
string name,
|
|
Transform parent,
|
|
out RectTransform content,
|
|
out DeveloperModeListRow template)
|
|
{
|
|
var root = CreatePanel(name, parent, new Color(0.02f, 0.035f, 0.05f, 1f));
|
|
var rootLayout = root.AddComponent<LayoutElement>();
|
|
rootLayout.flexibleHeight = 1f;
|
|
rootLayout.minHeight = 300f;
|
|
var scroll = root.AddComponent<ScrollRect>();
|
|
scroll.horizontal = false;
|
|
scroll.vertical = true;
|
|
scroll.movementType = ScrollRect.MovementType.Clamped;
|
|
scroll.scrollSensitivity = 24f;
|
|
|
|
var viewport = CreateUiObject("Viewport", typeof(RectMask2D));
|
|
viewport.transform.SetParent(root.transform, false);
|
|
Stretch(viewport.GetComponent<RectTransform>(), new Vector2(3f, 3f), new Vector2(-3f, -3f));
|
|
var contentObject = CreateUiObject(
|
|
"Content",
|
|
typeof(VerticalLayoutGroup),
|
|
typeof(ContentSizeFitter));
|
|
contentObject.transform.SetParent(viewport.transform, false);
|
|
content = contentObject.GetComponent<RectTransform>();
|
|
content.anchorMin = new Vector2(0f, 1f);
|
|
content.anchorMax = new Vector2(1f, 1f);
|
|
content.pivot = new Vector2(0.5f, 1f);
|
|
content.sizeDelta = Vector2.zero;
|
|
var vertical = contentObject.GetComponent<VerticalLayoutGroup>();
|
|
vertical.spacing = 2f;
|
|
vertical.childControlWidth = true;
|
|
vertical.childForceExpandWidth = true;
|
|
vertical.childControlHeight = true;
|
|
vertical.childForceExpandHeight = false;
|
|
contentObject.GetComponent<ContentSizeFitter>().verticalFit =
|
|
ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
var row = CreateUiObject(
|
|
"Row Template",
|
|
typeof(CanvasRenderer),
|
|
typeof(Image),
|
|
typeof(Button),
|
|
typeof(LayoutElement),
|
|
typeof(DeveloperModeListRow));
|
|
row.transform.SetParent(content, false);
|
|
var rowImage = row.GetComponent<Image>();
|
|
rowImage.color = Surface;
|
|
var rowButton = row.GetComponent<Button>();
|
|
rowButton.targetGraphic = rowImage;
|
|
var rowLayout = row.GetComponent<LayoutElement>();
|
|
rowLayout.preferredHeight = 30f;
|
|
rowLayout.minHeight = 30f;
|
|
var rowText = CreateText("Label", row.transform, "Template", 13f);
|
|
Stretch(rowText.rectTransform, new Vector2(7f, 2f), new Vector2(-7f, -2f));
|
|
rowText.alignment = TextAlignmentOptions.MidlineLeft;
|
|
rowText.enableWordWrapping = false;
|
|
var serialized = new SerializedObject(row.GetComponent<DeveloperModeListRow>());
|
|
Set(serialized, "button", rowButton);
|
|
Set(serialized, "label", rowText);
|
|
Set(serialized, "background", rowImage);
|
|
serialized.ApplyModifiedPropertiesWithoutUndo();
|
|
row.SetActive(false);
|
|
template = row.GetComponent<DeveloperModeListRow>();
|
|
scroll.viewport = viewport.GetComponent<RectTransform>();
|
|
scroll.content = content;
|
|
}
|
|
|
|
private static void SetAnchors(
|
|
RectTransform rect,
|
|
Vector2 min,
|
|
Vector2 max,
|
|
Vector2 offsetMin,
|
|
Vector2 offsetMax)
|
|
{
|
|
rect.anchorMin = min;
|
|
rect.anchorMax = max;
|
|
rect.offsetMin = offsetMin;
|
|
rect.offsetMax = offsetMax;
|
|
}
|
|
|
|
private static void Stretch(
|
|
RectTransform rect,
|
|
Vector2 offsetMin,
|
|
Vector2 offsetMax)
|
|
{
|
|
rect.anchorMin = Vector2.zero;
|
|
rect.anchorMax = Vector2.one;
|
|
rect.offsetMin = offsetMin;
|
|
rect.offsetMax = offsetMax;
|
|
}
|
|
|
|
private static void Stretch(RectTransform rect)
|
|
{
|
|
Stretch(rect, Vector2.zero, Vector2.zero);
|
|
}
|
|
|
|
private static void AddLayout(GameObject target, float height)
|
|
{
|
|
var layout = target.GetComponent<LayoutElement>() ?? target.AddComponent<LayoutElement>();
|
|
layout.preferredHeight = height;
|
|
}
|
|
|
|
private static void Set(SerializedObject serialized, string propertyName, UnityEngine.Object value)
|
|
{
|
|
var property = serialized.FindProperty(propertyName)
|
|
?? throw new MissingFieldException(
|
|
serialized.targetObject.GetType().Name,
|
|
propertyName);
|
|
property.objectReferenceValue = value;
|
|
}
|
|
}
|
|
}
|
|
#endif
|