diff --git a/.gitignore b/.gitignore index 16dfad5fc..d7073708f 100644 --- a/.gitignore +++ b/.gitignore @@ -78,9 +78,6 @@ crashlytics-build.properties # Local Save Files /[Aa]ssets/[Ss]treamingAssets/SaveFiles/* -# DevSaveFiles lives outside Assets; Unity metadata from the old location is not needed. -/DevSaveFiles/**/*.meta - # Never ignore DLLs in the FMOD subfolder. !/[Aa]ssets/Plugins/FMOD/**/lib/* diff --git a/Assets/AddressableAssetsData/AssetGroups/Scene_HuoshanFix.asset b/Assets/AddressableAssetsData/AssetGroups/Scene_HuoshanFix.asset index 5d71c7069..9cfb47a0f 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Scene_HuoshanFix.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Scene_HuoshanFix.asset @@ -32,11 +32,6 @@ MonoBehaviour: m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 - - m_GUID: 417a7984630786f46a5ade5d2b457852 - m_Address: "Animation/\u706B\u5C71" - m_ReadOnly: 0 - m_SerializedLabels: [] - FlaggedDuringContentUpdateRestriction: 0 - m_GUID: 838bd5cc2ceffc1459d167675d96f66a m_Address: "Timeline/\u706B\u5C71\u8868\u8FBEpanel" m_ReadOnly: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset b/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset index ccf825a58..13c1477ca 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset @@ -47,6 +47,11 @@ MonoBehaviour: m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 66be33a1bdb16ee409c6ac05b61e08a7 + m_Address: "Subway/\u76F8\u9519\u5217\u8F66" + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 0 m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} m_SchemaSet: diff --git a/Assets/GameContent/Huoshan/animation.meta b/Assets/Editor/AIBIS.meta similarity index 77% rename from Assets/GameContent/Huoshan/animation.meta rename to Assets/Editor/AIBIS.meta index 65e4eb6f3..907771bfa 100644 --- a/Assets/GameContent/Huoshan/animation.meta +++ b/Assets/Editor/AIBIS.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e2e0e2274704e494eb1cf554e2538516 +guid: 638cd3fa1c2470d42aceb69214c01786 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Editor/ChapterGraph/ChapterGraphEditorWindow.cs b/Assets/Editor/ChapterGraph/ChapterGraphEditorWindow.cs index 0ae2735be..098c7da8d 100644 --- a/Assets/Editor/ChapterGraph/ChapterGraphEditorWindow.cs +++ b/Assets/Editor/ChapterGraph/ChapterGraphEditorWindow.cs @@ -22,7 +22,7 @@ namespace AibisDream.SystemEditor private DropdownField _folderDropdown; private readonly Dictionary _displayToPath = new(); - [MenuItem("Window/Chapter Graph Editor")] + [MenuItem(AibisEditorMenus.ChapterGraphEditor)] public static void ShowWindow() { var window = GetWindow("Chapter Graph Editor"); diff --git a/Assets/GameContent/Huoshan/animationRaw.meta b/Assets/Editor/DeveloperMode.meta similarity index 77% rename from Assets/GameContent/Huoshan/animationRaw.meta rename to Assets/Editor/DeveloperMode.meta index 3a01e9e82..5774f9757 100644 --- a/Assets/GameContent/Huoshan/animationRaw.meta +++ b/Assets/Editor/DeveloperMode.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ed8d66d5054fd594b8a86bb4c9d8685f +guid: 665561f3805a96446b45ea1d50bfe520 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs b/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs new file mode 100644 index 000000000..1754eead2 --- /dev/null +++ b/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs @@ -0,0 +1,117 @@ +#if UNITY_EDITOR +using System.Linq; +using AibisDream.Kit; +using AibisDream.UI; +using AibisDream.Utility; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; + +namespace AibisDream.DeveloperMode.Editor +{ + public sealed class DeveloperModeCoreTests + { + [Test] + public void RuntimeLogBufferEvictsOldestAndPreservesOrder() + { + var buffer = new RuntimeLogBuffer(3); + buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, "one")); + buffer.Add(LogEntry.System(LogLevel.Warning, LogCategory.Save, "two")); + buffer.Add(LogEntry.System(LogLevel.Error, LogCategory.Yarn, "three")); + buffer.Add(LogEntry.System(LogLevel.Fatal, LogCategory.Scene, "four")); + + var snapshot = buffer.Snapshot(); + Assert.That(snapshot.Select(item => item.Message), Is.EqualTo(new[] { "two", "three", "four" })); + Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending); + Assert.That(snapshot.Length, Is.EqualTo(buffer.Capacity)); + } + + [Test] + public void RuntimeLogBufferAcceptsConcurrentWritersAndClears() + { + var buffer = new RuntimeLogBuffer(128); + System.Threading.Tasks.Parallel.For(0, 1000, index => + buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, index.ToString()))); + + var snapshot = buffer.Snapshot(); + Assert.That(snapshot.Length, Is.EqualTo(128)); + Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending); + buffer.Clear(); + Assert.That(buffer.Snapshot(), Is.Empty); + } + + [Test] + public void YarnStorageTypedSettersAndDebugSnapshotAreReadOnlyAndConsistent() + { + var go = new GameObject("YarnVariableStorage Test"); + try + { + var storage = go.AddComponent(); + storage.SetValue("$score", 12.5f); + storage.SetValue("$global_flag", true); + storage.SetValue("$name", "AIBIS"); + + var snapshot = DeveloperVariableSnapshot.Capture(storage, null); + Assert.That(snapshot.Select(item => item.Name), + Is.EquivalentTo(new[] { "$score", "$global_flag", "$name" })); + Assert.That(snapshot.Single(item => item.Name == "$score").Value, Is.EqualTo("12.5")); + Assert.That(snapshot.Single(item => item.Name == "$global_flag").IsGlobal, Is.True); + Assert.That(snapshot.All(item => item.HasRuntimeOverride), Is.True); + + storage.ClearLocal(); + var remaining = DeveloperVariableSnapshot.Capture(storage, null); + Assert.That(remaining.Select(item => item.Name), Is.EqualTo(new[] { "$global_flag" })); + storage.Clear(); + Assert.That(DeveloperVariableSnapshot.Capture(storage, null), Is.Empty); + } + finally + { + Object.DestroyImmediate(go); + } + } + + [Test] + public void EditorBuildEnablesDeveloperMode() + { + Assert.That(DeveloperModeGate.IsEnabled, Is.True); + } + + [Test] + public void PrefabIsConfiguredAndPersistenceReferencesIt() + { + const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab"; + const string scenePath = "Assets/Scenes/Persistence.unity"; + var prefab = AssetDatabase.LoadAssetAtPath(prefabPath); + Assert.That(prefab, Is.Not.Null); + Assert.That(prefab.activeSelf, Is.False); + var panel = prefab.GetComponent(); + Assert.That(panel, Is.Not.Null); + Assert.That(panel.IsConfigured, Is.True); + Assert.That(prefab.GetComponent().sizeDelta, Is.EqualTo(new Vector2(840f, 760f))); + Assert.That(AssetDatabase.GetDependencies(scenePath), Does.Contain(prefabPath)); + } + + [Test] + public void PanelToggleIsNonModalAndEscapeClosesIt() + { + const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab"; + var prefab = AssetDatabase.LoadAssetAtPath(prefabPath); + var instance = Object.Instantiate(prefab); + try + { + var panel = instance.GetComponent(); + var originalScale = Time.timeScale; + panel.TogglePanel(); + Assert.That(panel.IsOpen, Is.True); + Assert.That(Time.timeScale, Is.EqualTo(originalScale)); + Assert.That(panel.HandleEscape(), Is.True); + Assert.That(panel.IsOpen, Is.False); + } + finally + { + Object.DestroyImmediate(instance); + } + } + } +} +#endif diff --git a/Assets/Editor/FrameAnimation/FrameAnimationRuntimeSampleBuilder.cs.meta b/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs.meta similarity index 83% rename from Assets/Editor/FrameAnimation/FrameAnimationRuntimeSampleBuilder.cs.meta rename to Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs.meta index f3de5094b..71b3a549c 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationRuntimeSampleBuilder.cs.meta +++ b/Assets/Editor/DeveloperMode/DeveloperModeCoreTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 679ccd2bc27e67c468f002c1282b6045 +guid: b95ba9d1e80e3dc4e83bc408569c4955 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs b/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs new file mode 100644 index 000000000..d1ef08196 --- /dev/null +++ b/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs @@ -0,0 +1,235 @@ +#if UNITY_EDITOR +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AibisDream.SaveSystem; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace AibisDream.DeveloperMode.Editor.Tests +{ + public sealed class TestSaveSystemTests + { + private string _root; + private TestSaveRepository _repository; + + [SetUp] + public void SetUp() + { + _root = Path.Combine( + Path.GetTempPath(), + "AibisDream-TestSaveTests", + Guid.NewGuid().ToString("N")); + _repository = new TestSaveRepository(_root); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_root)) + { + Directory.Delete(_root, true); + } + } + + [Test] + public void Record_SameAnchor_ReplacesSnapshotAndKeepsFirstSeenOrder() + { + var first = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneOne")); + var second = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneTwo")); + var scan = _repository.Scan("1.0"); + + Assert.That(scan.Entries.Count, Is.EqualTo(1)); + Assert.That(second.Meta.firstSeenOrder, Is.EqualTo(first.Meta.firstSeenOrder)); + Assert.That(scan.Entries[0].Meta.sceneName, Is.EqualTo("SceneTwo")); + Assert.That(_repository.TryLoad(scan.Entries[0], out var snapshot, out var error), Is.True, error); + Assert.That(snapshot.scene.sceneName, Is.EqualTo("SceneTwo")); + } + + [Test] + public void Record_ReplacingEarlierEntry_DoesNotMoveItsOrder() + { + _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); + _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB")); + _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA2")); + + var entries = _repository.Scan().Entries.ToArray(); + Assert.That(entries.Select(item => item.NodeName), Is.EqualTo(new[] { "NodeA", "NodeB" })); + Assert.That(entries[0].Meta.firstSeenOrder, Is.LessThan(entries[1].Meta.firstSeenOrder)); + } + + [Test] + public void Scan_RetainsMissingSnapshotAsInvalid() + { + var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); + File.Delete(entry.SnapshotPath); + + var scan = _repository.Scan(); + + Assert.That(scan.ValidCount, Is.Zero); + Assert.That(scan.InvalidCount, Is.EqualTo(1)); + Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.MissingSnapshot)); + } + + [Test] + public void Scan_RetainsOrphanSnapshotWithBrokenMeta() + { + var directory = Path.Combine(_root, "chapter", "broken"); + Directory.CreateDirectory(directory); + File.WriteAllText( + Path.Combine(directory, TestSaveRepository.SnapshotFileName), + "{}", + Encoding.UTF8); + + var scan = _repository.Scan(); + + Assert.That(scan.InvalidCount, Is.EqualTo(1)); + Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptMeta)); + } + + [Test] + public void Validate_CorruptSnapshot_IsRetainedAndDisabledOnNextScan() + { + var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); + File.WriteAllText(entry.SnapshotPath, "{broken", Encoding.UTF8); + + Assert.That(_repository.Validate(entry, out var error), Is.False); + var rescanned = _repository.Scan(); + + Assert.That(error, Does.Contain("无法解析")); + Assert.That(rescanned.InvalidCount, Is.EqualTo(1)); + Assert.That(rescanned.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptSnapshot)); + } + + [Test] + public void Scan_RecoversCompleteStagingDirectory() + { + var final = Path.Combine(_root, "chapter", "node"); + var staging = final + ".__staging"; + Directory.CreateDirectory(staging); + var request = CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"); + var meta = CreateMeta(request, 1); + File.WriteAllText( + Path.Combine(staging, TestSaveRepository.MetaFileName), + JsonConvert.SerializeObject(meta), + Encoding.UTF8); + File.WriteAllText( + Path.Combine(staging, TestSaveRepository.SnapshotFileName), + SnapshotPersistence.Serialize(request.Snapshot), + Encoding.UTF8); + + var scan = _repository.Scan(); + + Assert.That(Directory.Exists(final), Is.True); + Assert.That(Directory.Exists(staging), Is.False); + Assert.That(scan.ValidCount, Is.EqualTo(1)); + } + + [Test] + public void Record_ConcurrentRequests_ProducesCompleteUniqueEntries() + { + var tasks = Enumerable.Range(0, 12) + .Select(index => Task.Run(() => + _repository.Record(CreateRequest( + "ChapterA", + "ProjectA", + $"Node{index:00}", + $"Scene{index:00}")))) + .ToArray(); + + Task.WaitAll(tasks); + var scan = _repository.Scan(); + + Assert.That(scan.ValidCount, Is.EqualTo(12)); + Assert.That(scan.InvalidCount, Is.Zero); + Assert.That(scan.Entries.Select(item => item.Meta.firstSeenOrder).Distinct().Count(), Is.EqualTo(12)); + } + + [Test] + public void DeleteInvalid_RemovesOnlyInvalidEntries() + { + _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA")); + var invalid = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB")); + File.Delete(invalid.SnapshotPath); + + var removed = _repository.DeleteInvalid(out var error); + var scan = _repository.Scan(); + + Assert.That(error, Is.Null); + Assert.That(removed, Is.EqualTo(1)); + Assert.That(scan.ValidCount, Is.EqualTo(1)); + Assert.That(scan.InvalidCount, Is.Zero); + } + + [TestCase(new[] { "hub" }, true)] + [TestCase(new[] { "linear" }, true)] + [TestCase(new[] { "content" }, true)] + [TestCase(new[] { "event" }, false)] + [TestCase(new[] { "hub", "no_save" }, false)] + [TestCase(new string[0], true)] + public void SilentCoverageEvaluator_MatchesSavePointTagSemantics(string[] tags, bool expected) + { + var actual = SavePointEvaluator.EvaluateNodeTagsForAutoSaveSilently( + "Node", + tags, + out _); + Assert.That(actual, Is.EqualTo(expected)); + } + + private static TestSaveRecordRequest CreateRequest( + string sceneSoName, + string yarnProject, + string nodeName, + string sceneName) + { + var key = TestSaveRecorder.BuildDedupeKey(sceneSoName, yarnProject, nodeName); + return new TestSaveRecordRequest + { + Snapshot = new SaveSnapshot + { + gameVersion = "1.0", + savedAt = "2026-01-01 00:00:00", + scene = new SceneSnapshotDto { sceneName = sceneName }, + anchor = new AnchorSnapshot + { + sceneSoName = sceneSoName, + yarnProjectId = yarnProject, + nodeName = nodeName + } + }, + DedupeKey = key, + EntryId = TestSaveRepository.StableHash(key), + ChapterId = sceneSoName, + ChapterTitle = sceneSoName, + SceneSoName = sceneSoName, + YarnProjectId = yarnProject, + NodeName = nodeName, + SceneName = sceneName, + GameVersion = "1.0" + }; + } + + private static TestSaveMeta CreateMeta(TestSaveRecordRequest request, long order) + { + return new TestSaveMeta + { + entryId = request.EntryId, + dedupeKey = request.DedupeKey, + chapterId = request.ChapterId, + chapterTitle = request.ChapterTitle, + sceneSoName = request.SceneSoName, + yarnProjectId = request.YarnProjectId, + nodeName = request.NodeName, + sceneName = request.SceneName, + firstSeenOrder = order, + firstRecordedAt = "2026-01-01 00:00:00", + lastRecordedAt = "2026-01-01 00:00:00", + snapshotSchemaVersion = SaveSnapshotSchema.CurrentVersion, + gameVersion = request.GameVersion + }; + } + } +} +#endif diff --git a/Assets/Editor/FrameAnimation/FrameAnimationImportSampleBuilder.cs.meta b/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs.meta similarity index 83% rename from Assets/Editor/FrameAnimation/FrameAnimationImportSampleBuilder.cs.meta rename to Assets/Editor/DeveloperMode/TestSaveSystemTests.cs.meta index 60e442746..35ca4567c 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationImportSampleBuilder.cs.meta +++ b/Assets/Editor/DeveloperMode/TestSaveSystemTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 291df5abf0a29fd4ca1cb0b86eb24a9e +guid: cbbba0c3621cf204483f453bad2d75a4 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Editor/FrameAnimation/AibisDream.FrameAnimation.Editor.asmdef b/Assets/Editor/FrameAnimation/AibisDream.FrameAnimation.Editor.asmdef index 8a5bae64f..66c17e7dd 100644 --- a/Assets/Editor/FrameAnimation/AibisDream.FrameAnimation.Editor.asmdef +++ b/Assets/Editor/FrameAnimation/AibisDream.FrameAnimation.Editor.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "AibisDream.FrameAnimation.Editor", "references": [ "AibisDream.FrameAnimation.Runtime", + "AibisDream.Menus", "Unity.2D.Sprite.Editor" ], "includePlatforms": [ diff --git a/Assets/Editor/FrameAnimation/AsepriteJsonParser.cs b/Assets/Editor/FrameAnimation/AsepriteJsonParser.cs index 32803a79b..19fc1aaac 100644 --- a/Assets/Editor/FrameAnimation/AsepriteJsonParser.cs +++ b/Assets/Editor/FrameAnimation/AsepriteJsonParser.cs @@ -140,7 +140,7 @@ namespace AibisDream.FrameAnimation.Editor foreach (var item in array) { tags.Add(new AsepriteSourceTag( - item.Value("name") ?? string.Empty, + (item.Value("name") ?? string.Empty).Trim(), item.Value("from") ?? -1, item.Value("to") ?? -1, item.Value("direction") ?? string.Empty)); diff --git a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs index 59344eb67..1a21517d3 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs @@ -84,7 +84,7 @@ namespace AibisDream.FrameAnimation.Editor private double lastPreviewTick; private bool updatingPreviewUi; - [MenuItem("Window/Aibis Dream/Frame Animation Graph Editor")] + [MenuItem(AibisEditorMenus.FrameAnimationGraphEditor)] public static void ShowWindow() { var window = GetWindow(); diff --git a/Assets/Editor/FrameAnimation/FrameAnimationImportSampleBuilder.cs b/Assets/Editor/FrameAnimation/FrameAnimationImportSampleBuilder.cs deleted file mode 100644 index fc5cfd3e5..000000000 --- a/Assets/Editor/FrameAnimation/FrameAnimationImportSampleBuilder.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace AibisDream.FrameAnimation.Editor -{ - public static class FrameAnimationImportSampleBuilder - { - private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation/Import"; - private const string ReadOnlyTexturePath = SampleFolder + "/中文预切图.png"; - private const string WritableTexturePath = SampleFolder + "/中文自动切图.png"; - private const string ObjectJsonPath = SampleFolder + "/中文Object.json"; - private const string ArrayJsonPath = SampleFolder + "/中文Array.json"; - private const string GraphPath = SampleFolder + "/ImportSampleGraph.asset"; - private const string IdleNodeId = "40000000000000000000000000000001"; - private const string BlinkNodeId = "40000000000000000000000000000002"; - private const string SharedNodeId = "40000000000000000000000000000003"; - private const string IdleEdgeId = "40000000000000000000000000000011"; - private const string BlinkEdgeId = "40000000000000000000000000000012"; - private const string IdleFlowId = "SampleIdleToSharedFlow"; - private const string BlinkFlowId = "SampleBlinkToSharedFlow"; - - [MenuItem("Tools/Frame Animation/Rebuild Import Sample")] - private static void BuildFromMenu() - { - BuildFromCommandLine(); - } - - public static void BuildFromCommandLine() - { - EnsureFolder(SampleFolder); - if (AssetDatabase.LoadAssetAtPath(GraphPath) != null) - { - AssetDatabase.DeleteAsset(GraphPath); - } - - WriteTexture(ReadOnlyTexturePath); - WriteTexture(WritableTexturePath); - WriteTextAsset(ObjectJsonPath, CreateObjectJson("中文预切图.png", "待机", "眨眼")); - WriteTextAsset(ArrayJsonPath, CreateArrayJson("中文自动切图.png", "转身", "惊讶")); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - - var readOnlyTexture = AssetDatabase.LoadAssetAtPath(ReadOnlyTexturePath); - var objectJson = AssetDatabase.LoadAssetAtPath(ObjectJsonPath); - if (!AsepriteJsonParser.TryParse(objectJson.text, "sample-slicing", out var document, out var parseIssue)) - { - throw new InvalidOperationException(parseIssue.Message); - } - var slicingSource = new FrameAnimationImportSource( - "Prepare Read Only Texture", readOnlyTexture, objectJson, new Vector2(0.5f, 0.5f), true); - var slicingPreview = new FrameAnimationImportSourcePreview(slicingSource) { Document = document }; - var slicingPlan = FrameAnimationSpriteUtility.BuildPlan(slicingSource, document, slicingPreview); - if (slicingPreview.HasErrors) - { - throw new InvalidOperationException(string.Join("\n", System.Linq.Enumerable.Select( - slicingPreview.Issues, issue => issue.Message))); - } - FrameAnimationSpriteUtility.ApplyPlan(slicingPlan); - - var graph = ScriptableObject.CreateInstance(); - graph.Configure( - "ImportSampleGraph", - "Aseprite 导入与稳定刷新样例", - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - string.Empty); - graph.AddImportSource(new FrameAnimationImportSource( - "Object / 只读切图", - AssetDatabase.LoadAssetAtPath(ReadOnlyTexturePath), - AssetDatabase.LoadAssetAtPath(ObjectJsonPath), - new Vector2(0.5f, 0.5f), - false)); - graph.AddImportSource(new FrameAnimationImportSource( - "Array / 自动切图", - AssetDatabase.LoadAssetAtPath(WritableTexturePath), - AssetDatabase.LoadAssetAtPath(ArrayJsonPath), - new Vector2(0.5f, 0.5f), - true)); - AssetDatabase.CreateAsset(graph, GraphPath); - AssetDatabase.SaveAssets(); - - var preview = FrameAnimationImportService.PreviewAll(graph); - if (!FrameAnimationImportService.Apply(preview, true, out var error)) - { - throw new InvalidOperationException(error + "\n" + string.Join("\n", - System.Linq.Enumerable.SelectMany(preview.Sources, - source => System.Linq.Enumerable.Select(source.Issues, issue => issue.Message)))); - } - - ExtendForWorkspaceSample(graph); - EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - Selection.activeObject = graph; - Debug.Log($"Frame Animation import sample rebuilt: {GraphPath}"); - } - - [MenuItem("Tools/Frame Animation/Upgrade Phase 4 Sample (Non-Destructive)")] - public static void UpgradePhase4Sample() - { - var graph = AssetDatabase.LoadAssetAtPath(GraphPath); - if (graph == null) - { - Debug.LogError($"Frame Animation sample Graph not found: {GraphPath}"); - return; - } - var idleClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "待机"); - var blinkClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "眨眼"); - var sharedClip = AssetDatabase.LoadAssetAtPath( - "Assets/GameContent/Test/FrameAnimation/Idle.asset") ?? - graph.Clips.FirstOrDefault(clip => clip != null && !clip.IsImported); - if (idleClip == null || blinkClip == null || sharedClip == null || !graph.Clips.Contains(sharedClip)) - { - Debug.LogError("Phase 4 sample upgrade requires 待机、眨眼 and a Manual Clip already referenced by the Graph."); - return; - } - - Undo.RecordObject(graph, "Upgrade Frame Animation Phase 4 Sample"); - var idleNode = GetOrAddNode(graph, IdleNodeId, idleClip, "待机 Node"); - var blinkNode = GetOrAddNode(graph, BlinkNodeId, blinkClip, "眨眼 Node"); - var sharedNode = graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == SharedNodeId); - if (sharedNode == null) - { - sharedNode = new AnimationNode(sharedClip.Id, "Shared Idle Node", - FrameClipEndBehavior.Loop, internalId: SharedNodeId); - graph.AddNode(sharedNode); - } - GetOrAddEdge(graph, IdleEdgeId, idleNode, sharedNode); - GetOrAddEdge(graph, BlinkEdgeId, blinkNode, sharedNode); - GetOrAddFlow(graph, IdleFlowId, "待机到共享 Idle", idleNode, - new Color(0.24f, 0.65f, 1f)); - GetOrAddFlow(graph, BlinkFlowId, "眨眼到共享 Idle", blinkNode, - new Color(1f, 0.62f, 0.24f)); - graph.EditorData.GetOrCreateNodeData(IdleNodeId, new Vector2(0f, 0f)); - graph.EditorData.GetOrCreateNodeData(BlinkNodeId, new Vector2(0f, 220f)); - graph.EditorData.GetOrCreateNodeData(SharedNodeId, new Vector2(360f, 110f)); - EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); - Selection.activeObject = graph; - Debug.Log("Frame Animation Phase 4 sample upgraded without rebuilding existing assets or sources."); - } - - private static AnimationNode GetOrAddNode( - FrameAnimationGraph graph, - string internalId, - FrameClip clip, - string displayName) - { - var node = graph.Nodes.FirstOrDefault(item => item != null && item.InternalId == internalId); - if (node != null) - { - return node; - } - node = new AnimationNode(clip.Id, displayName, internalId: internalId); - graph.AddNode(node); - return node; - } - - private static void GetOrAddEdge( - FrameAnimationGraph graph, - string internalId, - AnimationNode from, - AnimationNode to) - { - if (graph.Edges.Any(edge => edge != null && edge.InternalId == internalId)) - { - return; - } - graph.AddEdge(new AnimationEdge(from.InternalId, to.InternalId, internalId)); - } - - private static void GetOrAddFlow( - FrameAnimationGraph graph, - string id, - string displayName, - AnimationNode entry, - Color color) - { - var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == id); - if (flow == null && FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)) - { - flow = new AnimationFlow(id, displayName, entry.InternalId); - graph.AddFlow(flow); - } - if (flow != null) - { - graph.EditorData.GetOrCreateFlowData(flow.Id, color); - } - } - - private static void ExtendForWorkspaceSample(FrameAnimationGraph graph) - { - var imported = graph.Clips.First(clip => clip.ImportInfo?.SourceTagName == "待机"); - var manual = ScriptableObject.CreateInstance(); - manual.name = "ManualWorkspaceSample"; - manual.Configure( - "ManualWorkspaceSample", - "Graph 内 Manual 样例", - imported.Frames.Take(1).Select(frame => new FrameAnimationFrame( - frame.Sprite, frame.DurationMs, string.Empty, -1)), - 1f, - FrameClipEndBehavior.HoldLastFrame); - AssetDatabase.AddObjectToAsset(manual, graph); - - var clips = graph.Clips.Concat(new[] { manual }).ToList(); - var external = AssetDatabase.LoadAssetAtPath( - "Assets/GameContent/Test/FrameAnimation/Idle.asset"); - if (external != null && clips.All(clip => clip.Id != external.Id)) - { - clips.Add(external); - } - - var importedNode = new AnimationNode(imported.Id, "Imported 待机"); - var terminalClip = external != null && clips.Contains(external) ? external : manual; - var terminalNode = new AnimationNode( - terminalClip.Id, - "共享 Manual 终点", - endBehaviorOverride: FrameClipEndBehavior.Loop); - var edge = new AnimationEdge(importedNode.InternalId, terminalNode.InternalId); - var flow = new AnimationFlow( - "WorkspaceSampleFlow", - "工作台引用定位样例", - importedNode.InternalId); - graph.Configure( - graph.Id, - graph.DisplayName, - clips, - new[] { importedNode, terminalNode }, - new[] { edge }, - new[] { flow }, - flow.Id); - } - - private static JObject CreateFrame(string name, int x, int duration) - { - return new JObject - { - ["filename"] = name, - ["frame"] = new JObject { ["x"] = x, ["y"] = 0, ["w"] = 2, ["h"] = 2 }, - ["rotated"] = false, - ["trimmed"] = false, - ["spriteSourceSize"] = new JObject { ["x"] = 0, ["y"] = 0, ["w"] = 2, ["h"] = 2 }, - ["sourceSize"] = new JObject { ["w"] = 2, ["h"] = 2 }, - ["duration"] = duration - }; - } - - private static string CreateObjectJson(string imageName, string firstTag, string secondTag) - { - var root = CreateRoot(imageName, firstTag, secondTag); - root["frames"] = new JObject - { - ["中文帧_00"] = WithoutFilename(CreateFrame("中文帧_00", 0, 160)), - ["中文帧_01"] = WithoutFilename(CreateFrame("中文帧_01", 2, 90)) - }; - return root.ToString(Formatting.Indented); - } - - private static string CreateArrayJson(string imageName, string firstTag, string secondTag) - { - var root = CreateRoot(imageName, firstTag, secondTag); - root["frames"] = new JArray( - CreateFrame("自动帧_00", 0, 140), - CreateFrame("自动帧_01", 2, 110)); - return root.ToString(Formatting.Indented); - } - - private static JObject CreateRoot(string imageName, string firstTag, string secondTag) - { - return new JObject - { - ["meta"] = new JObject - { - ["image"] = imageName, - ["size"] = new JObject { ["w"] = 4, ["h"] = 2 }, - ["frameTags"] = new JArray( - new JObject - { - ["name"] = firstTag, ["from"] = 0, ["to"] = 0, ["direction"] = "forward" - }, - new JObject - { - ["name"] = secondTag, ["from"] = 0, ["to"] = 1, ["direction"] = "pingpong" - }) - } - }; - } - - private static JObject WithoutFilename(JObject frame) - { - frame.Remove("filename"); - return frame; - } - - private static void WriteTexture(string path) - { - var texture = new Texture2D(4, 2, TextureFormat.RGBA32, false); - texture.SetPixels(new[] - { - Color.cyan, Color.cyan, Color.magenta, Color.magenta, - Color.cyan, Color.cyan, Color.magenta, Color.magenta - }); - texture.Apply(); - File.WriteAllBytes(path, texture.EncodeToPNG()); - UnityEngine.Object.DestroyImmediate(texture); - } - - private static void WriteTextAsset(string path, string content) - { - File.WriteAllText(path, content, new UTF8Encoding(false)); - } - - private static void EnsureFolder(string path) - { - var segments = path.Split('/'); - var current = segments[0]; - for (var index = 1; index < segments.Length; index++) - { - var next = current + "/" + segments[index]; - if (!AssetDatabase.IsValidFolder(next)) - { - AssetDatabase.CreateFolder(current, segments[index]); - } - current = next; - } - } - } -} diff --git a/Assets/Editor/FrameAnimation/FrameAnimationPreviewSampleBuilder.cs b/Assets/Editor/FrameAnimation/FrameAnimationPreviewSampleBuilder.cs deleted file mode 100644 index 2e92404d1..000000000 --- a/Assets/Editor/FrameAnimation/FrameAnimationPreviewSampleBuilder.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Linq; -using UnityEditor; -using UnityEngine; - -namespace AibisDream.FrameAnimation.Editor -{ - public static class FrameAnimationPreviewSampleBuilder - { - private const string Folder = "Assets/GameContent/Test/FrameAnimation/Preview"; - private const string GraphPath = Folder + "/PreviewSampleGraph.asset"; - private const string TexturePath = "Assets/GameContent/Test/AnimatorRaw/手接树叶特写.png"; - - [MenuItem("Tools/Frame Animation/Create Phase 5 Preview Sample")] - public static void BuildFromCommandLine() - { - if (AssetDatabase.LoadAssetAtPath(GraphPath) != null) - { - Debug.Log($"Frame Animation preview sample already exists; preserved without overwrite: {GraphPath}"); - return; - } - EnsureFolder(Folder); - var sprites = AssetDatabase.LoadAllAssetsAtPath(TexturePath).OfType() - .OrderBy(sprite => sprite.name, StringComparer.Ordinal).ToArray(); - if (sprites.Length < 6) - { - throw new InvalidOperationException("第五阶段预览样例需要至少 6 个已切分 Sprite。"); - } - - var graph = ScriptableObject.CreateInstance(); - graph.name = "PreviewSampleGraph"; - AssetDatabase.CreateAsset(graph, GraphPath); - var action = CreateClip(graph, "PreviewAction", "Preview Action", FrameClipEndBehavior.HoldLastFrame, - new[] - { - new FrameAnimationFrame(sprites[0], 100, sprites[0].name, 0), - new FrameAnimationFrame(sprites[1], 160, sprites[1].name, 1), - new FrameAnimationFrame(sprites[2], 220, sprites[2].name, 2) - }); - var withEmpty = CreateClip(graph, "PreviewEmpty", "Preview Empty Frame", FrameClipEndBehavior.HoldLastFrame, - new[] - { - new FrameAnimationFrame(sprites[3], 120, sprites[3].name, 3), - new FrameAnimationFrame(null, 300, "empty", -1), - new FrameAnimationFrame(sprites[4], 120, sprites[4].name, 4) - }); - var idle = CreateClip(graph, "PreviewIdle", "Preview Idle Loop", FrameClipEndBehavior.Loop, - new[] - { - new FrameAnimationFrame(sprites[4], 140, sprites[4].name, 4), - new FrameAnimationFrame(sprites[5], 140, sprites[5].name, 5) - }); - - var finiteA = new AnimationNode(action.Id, "Finite Action", speedOverride: 2f, - internalId: "51000000000000000000000000000001"); - var finiteB = new AnimationNode(withEmpty.Id, "Finite Empty", - endBehaviorOverride: FrameClipEndBehavior.HoldLastFrame, - internalId: "51000000000000000000000000000002"); - var loopA = new AnimationNode(action.Id, "Loop Intro", internalId: "51000000000000000000000000000003"); - var loopB = new AnimationNode(idle.Id, "Loop Terminal", - endBehaviorOverride: FrameClipEndBehavior.Loop, - internalId: "51000000000000000000000000000004"); - var blocked = new AnimationNode(withEmpty.Id, "Zero Speed Block", speedOverride: 0f, - internalId: "51000000000000000000000000000005"); - var clear = new AnimationNode(action.Id, "Clear Terminal", - endBehaviorOverride: FrameClipEndBehavior.Clear, - internalId: "51000000000000000000000000000006"); - var hidden = new AnimationNode(idle.Id, "Hide Terminal", - endBehaviorOverride: FrameClipEndBehavior.HideTarget, - internalId: "51000000000000000000000000000007"); - var finiteFlow = new AnimationFlow("PreviewFiniteFlow", "Finite Flow", finiteA.InternalId); - var loopFlow = new AnimationFlow("PreviewLoopFlow", "Loop Flow", loopA.InternalId); - var blockedFlow = new AnimationFlow("PreviewBlockedFlow", "Zero Speed Flow", blocked.InternalId); - var clearFlow = new AnimationFlow("PreviewClearFlow", "Clear Flow", clear.InternalId); - var hideFlow = new AnimationFlow("PreviewHideFlow", "Hide Flow", hidden.InternalId); - graph.Configure( - "PreviewSampleGraph", - "Phase 5 Preview Sample", - new[] { action, withEmpty, idle }, - new[] { finiteA, finiteB, loopA, loopB, blocked, clear, hidden }, - new[] - { - new AnimationEdge(finiteA.InternalId, finiteB.InternalId, "52000000000000000000000000000001"), - new AnimationEdge(loopA.InternalId, loopB.InternalId, "52000000000000000000000000000002") - }, - new[] { finiteFlow, loopFlow, blockedFlow, clearFlow, hideFlow }, - finiteFlow.Id); - - var positions = new[] - { - (finiteA, new Vector2(80f, 80f)), (finiteB, new Vector2(420f, 80f)), - (loopA, new Vector2(80f, 320f)), (loopB, new Vector2(420f, 320f)), - (blocked, new Vector2(80f, 560f)), (clear, new Vector2(420f, 560f)), - (hidden, new Vector2(760f, 560f)) - }; - foreach (var item in positions) - { - graph.EditorData.GetOrCreateNodeData(item.Item1.InternalId, item.Item2); - } - var colors = new[] - { - new Color(0.25f, 0.65f, 0.95f), new Color(0.55f, 0.4f, 0.9f), - new Color(0.9f, 0.55f, 0.2f), new Color(0.3f, 0.75f, 0.5f), new Color(0.85f, 0.35f, 0.45f) - }; - var flows = graph.Flows.ToArray(); - for (var index = 0; index < flows.Length; index++) - { - graph.EditorData.GetOrCreateFlowData(flows[index].Id, colors[index]); - } - EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - Debug.Log($"Frame Animation phase 5 preview sample created: {GraphPath}"); - } - - private static FrameClip CreateClip( - FrameAnimationGraph graph, - string id, - string displayName, - FrameClipEndBehavior endBehavior, - FrameAnimationFrame[] frames) - { - var clip = ScriptableObject.CreateInstance(); - clip.name = id; - clip.Configure(id, displayName, frames, 1f, endBehavior); - AssetDatabase.AddObjectToAsset(clip, graph); - return clip; - } - - private static void EnsureFolder(string path) - { - var segments = path.Split('/'); - var current = segments[0]; - for (var index = 1; index < segments.Length; index++) - { - var next = current + "/" + segments[index]; - if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, segments[index]); - current = next; - } - } - } -} diff --git a/Assets/Editor/FrameAnimation/FrameAnimationPreviewSampleBuilder.cs.meta b/Assets/Editor/FrameAnimation/FrameAnimationPreviewSampleBuilder.cs.meta deleted file mode 100644 index d9ab4c201..000000000 --- a/Assets/Editor/FrameAnimation/FrameAnimationPreviewSampleBuilder.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f280e66f096a410eaf3eae8cd9c01134 diff --git a/Assets/Editor/FrameAnimation/FrameAnimationRuntimeSampleBuilder.cs b/Assets/Editor/FrameAnimation/FrameAnimationRuntimeSampleBuilder.cs deleted file mode 100644 index 792bba3f9..000000000 --- a/Assets/Editor/FrameAnimation/FrameAnimationRuntimeSampleBuilder.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.SceneManagement; -using UnityEngine.UI; - -namespace AibisDream.FrameAnimation.Editor -{ - public static class FrameAnimationRuntimeSampleBuilder - { - private const string SourceTexturePath = - "Assets/GameContent/Test/AnimatorRaw/手接树叶特写.png"; - private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation"; - private const string IntroClipPath = SampleFolder + "/Intro.asset"; - private const string IdleClipPath = SampleFolder + "/Idle.asset"; - private const string FlowGraphPath = SampleFolder + "/FlowSampleGraph.asset"; - private const string DirectGraphPath = SampleFolder + "/DirectSampleGraph.asset"; - private const string ScenePath = "Assets/Scenes/FrameAnimationRuntimeTest.unity"; - - [MenuItem("Tools/Frame Animation/Rebuild Runtime Sample")] - private static void BuildFromMenu() - { - if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) - { - return; - } - - BuildFromCommandLine(); - } - - public static void BuildFromCommandLine() - { - EnsureFolder(SampleFolder); - var sprites = AssetDatabase.LoadAllAssetsAtPath(SourceTexturePath) - .OfType() - .OrderBy(sprite => ExtractFrameNumber(sprite.name)) - .ToArray(); - if (sprites.Length < 21) - { - throw new InvalidOperationException( - $"样例源 Texture 需要至少 21 个 Sprite,当前只找到 {sprites.Length} 个。"); - } - - var introClip = CreateOrLoadAsset(IntroClipPath); - introClip.Configure( - "Intro", - "Intro", - CreateFrames(sprites, 0, 5), - 1f, - FrameClipEndBehavior.HoldLastFrame); - EditorUtility.SetDirty(introClip); - - var idleClip = CreateOrLoadAsset(IdleClipPath); - idleClip.Configure( - "Idle", - "Idle", - CreateFrames(sprites, 5, 16), - 1f, - FrameClipEndBehavior.Loop); - EditorUtility.SetDirty(idleClip); - - var flowGraph = CreateOrLoadAsset(FlowGraphPath); - var introNodeId = FindExistingNodeId(flowGraph, introClip.Id); - var idleNodeId = FindExistingNodeId(flowGraph, idleClip.Id); - var introNode = new AnimationNode(introClip.Id, "Intro", internalId: introNodeId); - var idleNode = new AnimationNode( - idleClip.Id, - "Idle Loop", - FrameClipEndBehavior.Loop, - internalId: idleNodeId); - var flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId); - flowGraph.Configure( - "FlowSampleGraph", - "Flow Sample Graph", - new[] { introClip, idleClip }, - new[] { introNode, idleNode }, - new[] { new AnimationEdge(introNode.InternalId, idleNode.InternalId) }, - new[] { flow }, - flow.Id); - EditorUtility.SetDirty(flowGraph); - - var directGraph = CreateOrLoadAsset(DirectGraphPath); - directGraph.Configure( - "DirectSampleGraph", - "Direct Sample Graph", - new[] { introClip, idleClip }, - Array.Empty(), - Array.Empty(), - Array.Empty(), - idleClip.Id); - EditorUtility.SetDirty(directGraph); - - AssetDatabase.SaveAssets(); - BuildScene(flowGraph, directGraph); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - Debug.Log($"Frame Animation runtime sample rebuilt: {ScenePath}"); - } - - private static IEnumerable CreateFrames( - IReadOnlyList sprites, - int startIndex, - int count) - { - for (var offset = 0; offset < count; offset++) - { - var sourceIndex = startIndex + offset; - var sprite = sprites[sourceIndex]; - yield return new FrameAnimationFrame(sprite, 250, sprite.name, sourceIndex); - } - } - - private static void BuildScene( - FrameAnimationGraph flowGraph, - FrameAnimationGraph directGraph) - { - var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); - - var cameraObject = new GameObject("Main Camera", typeof(Camera)); - cameraObject.tag = "MainCamera"; - cameraObject.transform.position = new Vector3(0f, 0f, -10f); - var camera = cameraObject.GetComponent(); - camera.orthographic = true; - camera.orthographicSize = 6f; - camera.clearFlags = CameraClearFlags.SolidColor; - camera.backgroundColor = new Color(0.08f, 0.09f, 0.12f, 1f); - - var spriteObject = new GameObject("Flow Sample - SpriteRenderer", typeof(SpriteRenderer)); - spriteObject.transform.position = new Vector3(-3.5f, 0f, 0f); - spriteObject.transform.localScale = Vector3.one * 0.45f; - var spritePlayer = spriteObject.AddComponent(); - spritePlayer.ConfigureForAuthoring(flowGraph, true, 1f); - - var canvasObject = new GameObject( - "Direct Clip Sample Canvas", - typeof(Canvas), - typeof(CanvasScaler), - typeof(GraphicRaycaster)); - var canvas = canvasObject.GetComponent(); - canvas.renderMode = RenderMode.ScreenSpaceOverlay; - var scaler = canvasObject.GetComponent(); - scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; - scaler.referenceResolution = new Vector2(1280f, 720f); - - var imageObject = new GameObject( - "Direct Idle - Image", - typeof(RectTransform), - typeof(CanvasRenderer), - typeof(Image)); - imageObject.transform.SetParent(canvasObject.transform, false); - var rectTransform = imageObject.GetComponent(); - rectTransform.sizeDelta = new Vector2(384f, 216f); - rectTransform.anchoredPosition = new Vector2(300f, 0f); - var image = imageObject.GetComponent(); - image.preserveAspect = true; - var imagePlayer = imageObject.AddComponent(); - imagePlayer.ConfigureForAuthoring(directGraph, true, 1f); - - EditorSceneManager.MarkSceneDirty(scene); - if (!EditorSceneManager.SaveScene(scene, ScenePath)) - { - throw new InvalidOperationException($"无法保存样例场景:{ScenePath}"); - } - } - - private static T CreateOrLoadAsset(string path) where T : ScriptableObject - { - var asset = AssetDatabase.LoadAssetAtPath(path); - if (asset != null) - { - return asset; - } - - asset = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(asset, path); - return asset; - } - - private static string FindExistingNodeId(FrameAnimationGraph graph, string clipId) - { - var existing = graph.Nodes.FirstOrDefault(node => node != null && node.ClipId == clipId); - return existing != null && FrameAnimationValueUtility.IsValidInternalId(existing.InternalId) - ? existing.InternalId - : null; - } - - private static int ExtractFrameNumber(string spriteName) - { - if (string.IsNullOrEmpty(spriteName)) - { - return int.MaxValue; - } - - var spaceIndex = spriteName.LastIndexOf(' '); - var dotIndex = spriteName.LastIndexOf('.'); - if (spaceIndex >= 0 && dotIndex > spaceIndex && - int.TryParse(spriteName.Substring(spaceIndex + 1, dotIndex - spaceIndex - 1), out var number)) - { - return number; - } - - return int.MaxValue; - } - - private static void EnsureFolder(string path) - { - var segments = path.Split('/'); - var current = segments[0]; - for (var index = 1; index < segments.Length; index++) - { - var next = current + "/" + segments[index]; - if (!AssetDatabase.IsValidFolder(next)) - { - AssetDatabase.CreateFolder(current, segments[index]); - } - current = next; - } - } - } -} diff --git a/Assets/Editor/FrameAnimation/FrameClipEditorWindow.cs b/Assets/Editor/FrameAnimation/FrameClipEditorWindow.cs index de8fd1d7c..1ea066a22 100644 --- a/Assets/Editor/FrameAnimation/FrameClipEditorWindow.cs +++ b/Assets/Editor/FrameAnimation/FrameClipEditorWindow.cs @@ -16,7 +16,7 @@ namespace AibisDream.FrameAnimation.Editor private FrameClipImportPreview importPreview; private double lastUpdateTime; - [MenuItem("Window/AibisDream/Frame Animation/Frame Clip Editor")] + [MenuItem(AibisEditorMenus.FrameClipEditor)] public static void ShowWindow() { GetWindow("Frame Clip Editor"); diff --git a/Assets/Editor/GlitchNoiseAudioGenerator.cs b/Assets/Editor/GlitchNoiseAudioGenerator.cs deleted file mode 100644 index 8e4e1749a..000000000 --- a/Assets/Editor/GlitchNoiseAudioGenerator.cs +++ /dev/null @@ -1,149 +0,0 @@ -using UnityEngine; -using UnityEditor; -using System.IO; - -/// -/// 生成与 SpriteNoiseGlitch shader 三阶段对应的程序化音效。 -/// 菜单: Tools > Generate Glitch Noise Audio -/// -public static class GlitchNoiseAudioGenerator -{ - private const int SampleRate = 44100; - private const float Duration = 4f; // 每段音效时长(秒),便于循环使用 - private const string OutputFolder = "Assets/RawResources/Audio/GlitchNoise"; - - [MenuItem("Tools/Generate Glitch Noise Audio")] - public static void Generate() - { - string dir = Path.Combine(Application.dataPath, "RawResources", "Audio", "GlitchNoise"); - Directory.CreateDirectory(dir); - - GenerateStage1(dir); - GenerateStage2(dir); - GenerateStage3(dir); - - AssetDatabase.Refresh(); - Debug.Log($"[GlitchNoise] 已生成三阶段音效至 Assets/RawResources/Audio/GlitchNoise"); - } - - /// Stage 1: 轻微静态噪点 - 柔和白噪声 - private static void GenerateStage1(string dir) - { - int samples = (int)(SampleRate * Duration); - float[] data = new float[samples]; - var rnd = new System.Random(12345); - - float gain = 0.12f; - for (int i = 0; i < samples; i++) - { - data[i] = ((float)rnd.NextDouble() * 2f - 1f) * gain; - } - - SaveWav(dir, "GlitchNoise_Stage1_Light.wav", data); - } - - /// Stage 2: 中等 - 更多噪点 + 偶尔 glitch 爆音 - private static void GenerateStage2(string dir) - { - int samples = (int)(SampleRate * Duration); - float[] data = new float[samples]; - var rnd = new System.Random(23456); - - float noiseGain = 0.22f; - int glitchInterval = SampleRate / 4; // 约每 0.25 秒一次 glitch 爆音 - - for (int i = 0; i < samples; i++) - { - float n = ((float)rnd.NextDouble() * 2f - 1f) * noiseGain; - - // 随机 glitch 爆音 - if (i > 0 && i % glitchInterval < 120) - { - float burst = ((float)rnd.NextDouble() * 2f - 1f) * 0.5f; - n += burst * (1f - (i % glitchInterval) / 120f); - } - - data[i] = Mathf.Clamp(n, -1f, 1f); - } - - SaveWav(dir, "GlitchNoise_Stage2_Medium.wav", data); - } - - /// Stage 3: 严重 - 强烈噪点 + 频繁 glitch + 数字故障感 - private static void GenerateStage3(string dir) - { - int samples = (int)(SampleRate * Duration); - float[] data = new float[samples]; - var rnd = new System.Random(34567); - - float noiseGain = 0.4f; - int blockSize = 2205; // 约 0.05 秒一块 - int glitchBlockEvery = 4; - - for (int i = 0; i < samples; i++) - { - int block = i / blockSize; - float n = ((float)rnd.NextDouble() * 2f - 1f) * noiseGain; - - // 块状 glitch:整块随机反转/爆音 - if (block % glitchBlockEvery == 0) - { - int posInBlock = i % blockSize; - float t = (float)posInBlock / blockSize; - n += ((float)rnd.NextDouble() * 2f - 1f) * 0.6f * (1f - t); - } - - // 随机“卡顿”短静音后爆音 - if (rnd.NextDouble() < 0.0003) - { - int silenceLen = 100 + rnd.Next(300); - int end = Mathf.Min(i + silenceLen, samples); - for (int j = i; j < end; j++) - { - data[j] = j == end - 1 ? ((float)rnd.NextDouble() * 2f - 1f) * 0.8f : 0f; - } - i = end - 1; - } - else - { - data[i] = Mathf.Clamp(n, -1f, 1f); - } - } - - SaveWav(dir, "GlitchNoise_Stage3_Heavy.wav", data); - } - - private static void SaveWav(string dir, string filename, float[] samples) - { - string path = Path.Combine(dir, filename); - using (var fs = new FileStream(path, FileMode.Create)) - using (var bw = new BinaryWriter(fs)) - { - // RIFF header - bw.Write(new[] { 'R', 'I', 'F', 'F' }); - int dataSize = samples.Length * 2; // 16-bit - bw.Write(36 + dataSize); - bw.Write(new[] { 'W', 'A', 'V', 'E' }); - - // fmt chunk - bw.Write(new[] { 'f', 'm', 't', ' ' }); - bw.Write(16); // chunk size - bw.Write((short)1); // PCM - bw.Write((short)1); // mono - bw.Write(SampleRate); - bw.Write(SampleRate * 2); - bw.Write((short)2); - bw.Write((short)16); - - // data chunk - bw.Write(new[] { 'd', 'a', 't', 'a' }); - bw.Write(dataSize); - - foreach (float s in samples) - { - short sample = (short)Mathf.Clamp((int)(s * 32767), -32768, 32767); - bw.Write(sample); - } - } - } -} diff --git a/Assets/Editor/HandlerNameCollector/HandlerNameCollectorWindow.cs b/Assets/Editor/HandlerNameCollector/HandlerNameCollectorWindow.cs index 4f0920522..66177aebc 100644 --- a/Assets/Editor/HandlerNameCollector/HandlerNameCollectorWindow.cs +++ b/Assets/Editor/HandlerNameCollector/HandlerNameCollectorWindow.cs @@ -9,7 +9,7 @@ namespace AibisDream.EditorTools { public class HandlerNameCollectorWindow : EditorWindow { - private const string WindowTitle = "Handler Name Collector"; + private const string WindowTitle = "名称收集器"; private HandlerNameCache _cache; @@ -30,7 +30,7 @@ namespace AibisDream.EditorTools private HandlerNameCacheEntry _selectedEntry; - [MenuItem("Tools/Handler Name Collector")] + [MenuItem(AibisEditorMenus.NameCollector)] public static void ShowWindow() { var window = GetWindow(WindowTitle); @@ -38,12 +38,6 @@ namespace AibisDream.EditorTools window.Show(); } - [MenuItem("Tools/Timeline Name Collector", false, 101)] - public static void ShowWindowLegacyMenu() - { - ShowWindow(); - } - private void OnEnable() { LoadCacheIfNeeded(); diff --git a/Assets/Editor/MenuItem/BlockPuzzleMenuItemCreator.cs b/Assets/Editor/MenuItem/BlockPuzzleMenuItemCreator.cs index b7372eeea..a5510caae 100644 --- a/Assets/Editor/MenuItem/BlockPuzzleMenuItemCreator.cs +++ b/Assets/Editor/MenuItem/BlockPuzzleMenuItemCreator.cs @@ -7,7 +7,7 @@ namespace AibisDream.SystemEditor { private const string BLOCK_SHAPE_PREFAB_PATH = "Assets/Prefabs/BlockPuzzle/BlockShape.prefab"; - [MenuItem("GameObject/Block Puzzle/Shape")] + [MenuItem(AibisEditorMenus.BlockPuzzleShape)] public static BlockShape CreateBlockPuzzleShape() { var prefabAsset =AssetDatabase.LoadAssetAtPath(BLOCK_SHAPE_PREFAB_PATH); diff --git a/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs b/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs deleted file mode 100644 index e374c1698..000000000 --- a/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs +++ /dev/null @@ -1,536 +0,0 @@ -#if UNITY_EDITOR -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using AibisDream.SaveSystem; -using AibisDream.Utility; -using Newtonsoft.Json; -using NUnit.Framework; -using UnityEditor; -using UnityEditor.AddressableAssets; -using UnityEditor.AddressableAssets.Settings; -using UnityEngine; -using UnityEngine.TestTools; - -namespace AibisDream.EditorTools -{ - /// 从本机 testsavs 选择章节、去重排序并提升为可提交测试档。 - public sealed class DevSavePromoteWindow : EditorWindow - { - private readonly List groups = new(); - private readonly List preview = new(); - private int selectedGroup; - private string sectionId = string.Empty; - private string sectionTitle = string.Empty; - private string status = string.Empty; - private Vector2 scroll; - - [MenuItem("Tools/Aibis/Dev Save Promote")] - public static void Open() - { - var window = GetWindow("Dev Save Promote"); - window.minSize = new Vector2(720f, 520f); - window.Show(); - } - - private void OnEnable() => RefreshGroups(); - - private void OnGUI() - { - EditorGUILayout.LabelField("测试存档提升与校验", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "来源是本机 testsavs;目标是项目根目录的 DevSaveFiles。该目录可提交,但不会被 Unity 自动打包。", - MessageType.Info); - - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("刷新本机归档", GUILayout.Height(26f))) RefreshGroups(); - if (GUILayout.Button("校验全部已提交测试档", GUILayout.Height(26f))) ValidateCommittedCatalog(); - } - - if (groups.Count == 0) - { - EditorGUILayout.HelpBox("本机没有可用 testsavs。", MessageType.Warning); - DrawStatus(); - return; - } - - var labels = groups.Select(group => - $"{group.SceneSoName} | {group.YarnProjectId} | {group.SceneName} ({group.Items.Count})").ToArray(); - var nextGroup = EditorGUILayout.Popup("章节归档", Mathf.Clamp(selectedGroup, 0, labels.Length - 1), labels); - if (nextGroup != selectedGroup || preview.Count == 0) - { - selectedGroup = nextGroup; - SelectGroup(groups[selectedGroup]); - } - - sectionId = EditorGUILayout.TextField("Section Id", sectionId); - sectionTitle = EditorGUILayout.TextField("显示名称", sectionTitle); - EditorGUILayout.LabelField("规则", "每个 Yarn 节点保留 savedAt 最新一份;默认按 savedAt 升序"); - - using (new EditorGUILayout.HorizontalScope()) - { - GUILayout.FlexibleSpace(); - if (GUILayout.Button("提升当前章节", GUILayout.Width(180f), GUILayout.Height(28f))) Promote(); - } - - EditorGUILayout.Space(5f); - EditorGUILayout.LabelField($"阶段预览:{preview.Count}", EditorStyles.boldLabel); - scroll = EditorGUILayout.BeginScrollView(scroll); - for (var i = 0; i < preview.Count; i++) - { - var item = preview[i]; - using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) - { - EditorGUILayout.LabelField($"{i + 1:000}", GUILayout.Width(38f)); - item.Label = EditorGUILayout.TextField(item.Label, GUILayout.Width(210f)); - EditorGUILayout.LabelField(item.NodeName, GUILayout.MinWidth(180f)); - EditorGUILayout.LabelField(item.SavedAt, GUILayout.Width(140f)); - using (new EditorGUI.DisabledScope(i == 0)) - { - if (GUILayout.Button("↑", GUILayout.Width(26f))) Move(i, i - 1); - } - using (new EditorGUI.DisabledScope(i == preview.Count - 1)) - { - if (GUILayout.Button("↓", GUILayout.Width(26f))) Move(i, i + 1); - } - } - } - EditorGUILayout.EndScrollView(); - DrawStatus(); - } - - private void DrawStatus() - { - if (!string.IsNullOrWhiteSpace(status)) - EditorGUILayout.HelpBox(status, status.StartsWith("OK", StringComparison.Ordinal) ? MessageType.Info : MessageType.Warning); - } - - private void RefreshGroups() - { - groups.Clear(); - preview.Clear(); - var root = ConstRef.TestAutoSaveArchivePath; - if (!Directory.Exists(root)) - { - status = $"找不到本机归档:{root}"; - return; - } - - var candidates = new List(); - foreach (var directory in Directory.GetDirectories(root)) - { - var snapshotPath = Path.Combine(directory, $"{ConstRef.SaveSnapshotFileName}.json"); - if (!File.Exists(snapshotPath)) continue; - try - { - var snapshot = SnapshotPersistence.Load(Path.Combine(directory, ConstRef.SaveSnapshotFileName)); - if (snapshot?.anchor == null || string.IsNullOrWhiteSpace(snapshot.anchor.nodeName)) continue; - candidates.Add(new Candidate( - snapshotPath, - Path.Combine(directory, $"{ConstRef.SaveMetaFileName}.json"), - snapshot.scene?.sceneName, - snapshot.anchor.sceneSoName, - snapshot.anchor.yarnProjectId, - snapshot.anchor.nodeName, - snapshot.savedAt)); - } - catch (Exception ex) - { - Debug.LogWarning($"[DevSavePromote] 跳过损坏归档 {snapshotPath}: {ex.Message}"); - } - } - - groups.AddRange(candidates - .GroupBy(item => $"{item.SceneName}\n{item.SceneSoName}\n{item.YarnProjectId}", StringComparer.Ordinal) - .Select(group => new ArchiveGroup(group.ToList())) - .OrderBy(group => group.SceneSoName, StringComparer.Ordinal)); - selectedGroup = Mathf.Clamp(selectedGroup, 0, Math.Max(0, groups.Count - 1)); - if (groups.Count > 0) SelectGroup(groups[selectedGroup]); - status = $"OK:读取 {candidates.Count} 份归档,分为 {groups.Count} 个章节。"; - } - - private void SelectGroup(ArchiveGroup group) - { - preview.Clear(); - preview.AddRange(group.Items - .GroupBy(item => item.NodeName, StringComparer.Ordinal) - .Select(nodes => nodes.OrderByDescending(item => item.SavedAt, StringComparer.Ordinal).First()) - .OrderBy(item => item.SavedAt, StringComparer.Ordinal)); - sectionId = SanitizeIdentifier(group.SceneSoName); - sectionTitle = group.SceneSoName; - } - - private void Move(int from, int to) - { - if (from < 0 || to < 0 || from >= preview.Count || to >= preview.Count) return; - var item = preview[from]; - preview.RemoveAt(from); - preview.Insert(to, item); - GUI.FocusControl(null); - } - - private void Promote() - { - if (preview.Count == 0 || string.IsNullOrWhiteSpace(sectionId)) - { - status = "没有可提升阶段,或 Section Id 为空。"; - return; - } - - var validation = ValidateCandidates(preview); - if (validation.Count > 0) - { - status = string.Join("\n", validation); - return; - } - - var group = groups[selectedGroup]; - var root = ConstRef.TestSaveFilePath; - var targetSection = Path.Combine(root, sectionId); - if (Directory.Exists(targetSection)) Directory.Delete(targetSection, true); - Directory.CreateDirectory(targetSection); - - var catalog = LoadCatalogForWrite(); - catalog.sections.RemoveAll(section => string.Equals(section.id, sectionId, StringComparison.OrdinalIgnoreCase)); - var sectionDto = new DevSaveCatalogSectionDto - { - order = FindDemoChapterOrder(group.SceneSoName), - id = sectionId, - title = string.IsNullOrWhiteSpace(sectionTitle) ? sectionId : sectionTitle, - expectedSceneName = group.SceneName, - expectedSceneSoName = group.SceneSoName, - expectedYarnProjectId = group.YarnProjectId - }; - - for (var i = 0; i < preview.Count; i++) - { - var item = preview[i]; - var folder = $"{i + 1:000}_{SanitizeIdentifier(item.NodeName)}"; - var destination = Path.Combine(targetSection, folder); - Directory.CreateDirectory(destination); - File.Copy(item.SnapshotPath, Path.Combine(destination, $"{ConstRef.SaveSnapshotFileName}.json"), true); - if (File.Exists(item.MetaPath)) - File.Copy(item.MetaPath, Path.Combine(destination, $"{ConstRef.SaveMetaFileName}.json"), true); - sectionDto.entries.Add(new DevSaveCatalogEntryDto - { - order = i + 1, - label = string.IsNullOrWhiteSpace(item.Label) ? item.NodeName : item.Label, - path = $"{sectionId}/{folder}", - anchorNode = item.NodeName - }); - } - - catalog.sections.Add(sectionDto); - catalog.sections.Sort((left, right) => left.order.CompareTo(right.order)); - WriteCatalog(catalog); - AssetDatabase.Refresh(); - ValidateCommittedCatalog(); - } - - private static List ValidateCandidates(IEnumerable candidates) - { - var errors = new List(); - var nodes = new HashSet(StringComparer.Ordinal); - foreach (var item in candidates) - { - if (!nodes.Add(item.NodeName)) errors.Add($"重复节点:{item.NodeName}"); - try - { - var snapshot = SnapshotPersistence.Load(Path.Combine(Path.GetDirectoryName(item.SnapshotPath)!, ConstRef.SaveSnapshotFileName)); - if (snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion) - errors.Add($"{item.NodeName}: schema {snapshot.schemaVersion} 不兼容"); - } - catch (Exception ex) - { - errors.Add($"{item.NodeName}: {ex.Message}"); - } - } - return errors; - } - - private void ValidateCommittedCatalog() - { - var sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath); - var errors = new List(); - if (!string.IsNullOrWhiteSpace(DevSaveCatalog.LastError)) errors.Add(DevSaveCatalog.LastError); - foreach (var section in sections) - { - var orders = new HashSet(); - var nodes = new HashSet(StringComparer.Ordinal); - foreach (var entry in section.Entries) - { - if (!orders.Add(entry.Order)) errors.Add($"{section.Id}: 重复 order {entry.Order}"); - if (!nodes.Add(entry.AnchorNode)) errors.Add($"{section.Id}: 重复节点 {entry.AnchorNode}"); - if (!entry.IsValid) errors.Add($"{section.Id}/{entry.Label}: {entry.Error}"); - } - } - - var dto = LoadCatalogForWrite(); - foreach (var section in dto.sections) - { - if (!SceneAddressExists(section.expectedSceneName)) - errors.Add($"{section.id}: Addressable 场景不存在 {section.expectedSceneName}"); - var sceneSo = FindTalkSceneSo(section.expectedSceneSoName); - if (sceneSo == null) - errors.Add($"{section.id}: TalkSceneSO 不存在 {section.expectedSceneSoName}"); - else if (sceneSo.yarnProject == null || sceneSo.yarnProject.name != section.expectedYarnProjectId) - errors.Add($"{section.id}: TalkSceneSO 的 YarnProject 不匹配 {section.expectedYarnProjectId}"); - else - { - var nodeNames = new HashSet(sceneSo.yarnProject.NodeNames, StringComparer.Ordinal); - foreach (var entry in section.entries.Where(entry => !nodeNames.Contains(entry.anchorNode))) - errors.Add($"{section.id}: Yarn 节点不存在 {entry.anchorNode}"); - } - } - - status = errors.Count == 0 - ? $"OK:{sections.Count} 个章节、{sections.Sum(s => s.Entries.Count)} 个阶段全部通过校验。" - : $"校验失败({errors.Count}):\n{string.Join("\n", errors)}"; - if (errors.Count > 0) Debug.LogError($"[DevSavePromote] {status}"); - else Debug.Log($"[DevSavePromote] {status}"); - } - - private static bool SceneAddressExists(string address) - { - var settings = AddressableAssetSettingsDefaultObject.Settings; - return settings != null && settings.groups - .Where(group => group != null) - .SelectMany(group => group.entries) - .Any(entry => string.Equals(entry.address, address, StringComparison.Ordinal)); - } - - private static TalkSceneSO FindTalkSceneSo(string assetName) - { - foreach (var guid in AssetDatabase.FindAssets($"t:TalkSceneSO {assetName}")) - { - var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); - if (asset != null && asset.name == assetName) return asset; - } - return null; - } - - private static int FindDemoChapterOrder(string sceneSoName) - { - const string firstChapterPath = "Assets/ScriptableObjects/SceneSO/Demo/Day0_Prologue.asset"; - var chapter = AssetDatabase.LoadAssetAtPath(firstChapterPath); - var visited = new HashSet(); - var order = 1; - while (chapter != null && visited.Add(chapter)) - { - if (string.Equals(chapter.name, sceneSoName, StringComparison.Ordinal)) return order; - chapter = chapter.GetNextScene(); - order++; - } - - return int.MaxValue; - } - - private static DevSaveCatalogDto LoadCatalogForWrite() - { - var path = Path.Combine(ConstRef.TestSaveFilePath, "catalog.json"); - if (!File.Exists(path)) return new DevSaveCatalogDto(); - try - { - return JsonConvert.DeserializeObject(File.ReadAllText(path, Encoding.UTF8)) - ?? new DevSaveCatalogDto(); - } - catch - { - return new DevSaveCatalogDto(); - } - } - - private static void WriteCatalog(DevSaveCatalogDto catalog) - { - Directory.CreateDirectory(ConstRef.TestSaveFilePath); - File.WriteAllText( - Path.Combine(ConstRef.TestSaveFilePath, "catalog.json"), - JsonConvert.SerializeObject(catalog, Formatting.Indented), - new UTF8Encoding(false)); - } - - private static string SanitizeIdentifier(string value) - { - if (string.IsNullOrWhiteSpace(value)) return "unnamed"; - foreach (var character in Path.GetInvalidFileNameChars()) value = value.Replace(character, '_'); - value = value.Replace(' ', '_'); - return value.Length <= 48 ? value : value.Substring(0, 48); - } - - private sealed class ArchiveGroup - { - public readonly List Items; - public string SceneName => Items[0].SceneName; - public string SceneSoName => Items[0].SceneSoName; - public string YarnProjectId => Items[0].YarnProjectId; - public ArchiveGroup(List items) => Items = items; - } - - private sealed class Candidate - { - public readonly string SnapshotPath; - public readonly string MetaPath; - public readonly string SceneName; - public readonly string SceneSoName; - public readonly string YarnProjectId; - public readonly string NodeName; - public readonly string SavedAt; - public string Label; - - public Candidate(string snapshotPath, string metaPath, string sceneName, string sceneSoName, - string yarnProjectId, string nodeName, string savedAt) - { - SnapshotPath = snapshotPath; - MetaPath = metaPath; - SceneName = sceneName; - SceneSoName = sceneSoName; - YarnProjectId = yarnProjectId; - NodeName = nodeName; - SavedAt = savedAt ?? string.Empty; - Label = nodeName; - } - } - } - - [TestFixture] - public sealed class DevSaveCatalogTests - { - private string root; - - [SetUp] - public void SetUp() - { - root = Path.Combine(Path.GetTempPath(), $"aibis-dev-save-{Guid.NewGuid():N}"); - Directory.CreateDirectory(root); - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(root)) Directory.Delete(root, true); - } - - [Test] - public void CatalogUsesExplicitOrderAndKeepsInvalidEntriesVisible() - { - WriteCatalog(new DevSaveCatalogEntryDto { order = 20, label = "late", path = "S/late", anchorNode = "Late" }, - new DevSaveCatalogEntryDto { order = 10, label = "early", path = "S/early", anchorNode = "Early" }); - - var section = DevSaveCatalog.Load(root).Single(); - - Assert.That(section.Entries.Select(entry => entry.Label), Is.EqualTo(new[] { "early", "late" })); - Assert.That(section.Entries.All(entry => !entry.IsValid), Is.True); - Assert.That(section.Entries.All(entry => entry.Error.Contains("缺失")), Is.True); - } - - [Test] - public void CatalogRejectsPathTraversal() - { - WriteCatalog(new DevSaveCatalogEntryDto { order = 1, label = "unsafe", path = "../outside", anchorNode = "Start" }); - - var entry = DevSaveCatalog.Load(root).Single().Entries.Single(); - - Assert.That(entry.IsValid, Is.False); - Assert.That(entry.Error, Does.Contain("越过")); - } - - [Test] - public void CatalogReportsCorruptSchemaAndAnchorMismatch() - { - WriteSnapshot("S/corrupt", "{"); - WriteSnapshot("S/schema", JsonConvert.SerializeObject(BuildSnapshot("Node", schema: 99))); - WriteSnapshot("S/node", JsonConvert.SerializeObject(BuildSnapshot("Actual"))); - WriteCatalog( - new DevSaveCatalogEntryDto { order = 1, label = "corrupt", path = "S/corrupt", anchorNode = "Node" }, - new DevSaveCatalogEntryDto { order = 2, label = "schema", path = "S/schema", anchorNode = "Node" }, - new DevSaveCatalogEntryDto { order = 3, label = "node", path = "S/node", anchorNode = "Expected" }); - - var entries = DevSaveCatalog.Load(root).Single().Entries; - - Assert.That(entries[0].Error, Does.Contain("JSON")); - Assert.That(entries[1].Error, Does.Contain("版本")); - Assert.That(entries[2].Error, Does.Contain("节点不匹配")); - } - - [Test] - public void CommittedCatalogContainsAllDemoChaptersWithValidUniqueEntries() - { - var sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath); - Assert.That(DevSaveCatalog.LastError, Is.Null); - Assert.That(sections.Select(section => section.Id), Is.EqualTo(new[] - { - "Day0Prologue", "Day1Begin", "Peipei1", "Day1Mid", "Day1Night", "Day1Sleep", - "Day2Begin", "Huoshan1", "Day2Mid", "Peipei2", "Day2Night", "Day2Sleep", "GeneralEnd" - })); - foreach (var section in sections) - { - Assert.That(section.Entries.All(entry => entry.IsValid), Is.True, - string.Join("\n", section.Entries.Where(entry => !entry.IsValid).Select(entry => $"{entry.Label}: {entry.Error}"))); - Assert.That(section.Entries.Select(entry => entry.Order).Distinct().Count(), Is.EqualTo(section.Entries.Count)); - Assert.That(section.Entries.Select(entry => entry.DestinationNode).Distinct().Count(), Is.EqualTo(section.Entries.Count)); - } - } - - [Test] - public void StrictRestoreContextTracksPhaseWarningsAndErrors() - { - var context = new SnapshotRestoreContext(new SaveSnapshot(), strictMode: true); - context.SetPhase("Provider restore"); - context.Warn("optional state missing"); - LogAssert.Expect(LogType.Error, "[SnapshotRestore] required state missing"); - context.Error("required state missing"); - - Assert.That(context.StrictMode, Is.True); - Assert.That(context.CurrentPhase, Is.EqualTo("Provider restore")); - Assert.That(context.Warnings, Is.EqualTo(new[] { "optional state missing" })); - Assert.That(context.Errors, Is.EqualTo(new[] { "required state missing" })); - Assert.That(context.HasErrors, Is.True); - } - - private void WriteCatalog(params DevSaveCatalogEntryDto[] entries) - { - var catalog = new DevSaveCatalogDto - { - sections = new List - { - new() - { - id = "S", - title = "Section", - expectedSceneName = "Scene/Test", - expectedSceneSoName = "TestSO", - expectedYarnProjectId = "TestYarn", - entries = entries.ToList() - } - } - }; - File.WriteAllText(Path.Combine(root, "catalog.json"), JsonConvert.SerializeObject(catalog)); - } - - private void WriteSnapshot(string relativeDirectory, string json) - { - var directory = Path.Combine(root, relativeDirectory.Replace('/', Path.DirectorySeparatorChar)); - Directory.CreateDirectory(directory); - File.WriteAllText(Path.Combine(directory, "snapshot.json"), json); - } - - private static SaveSnapshot BuildSnapshot(string node, int schema = SaveSnapshotSchema.CurrentVersion) - { - return new SaveSnapshot - { - schemaVersion = schema, - scene = new SceneSnapshotDto { sceneName = "Scene/Test" }, - anchor = new AnchorSnapshot - { - sceneSoName = "TestSO", - yarnProjectId = "TestYarn", - nodeName = node - } - }; - } - } -} -#endif diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs b/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs deleted file mode 100644 index 134b3df22..000000000 --- a/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs +++ /dev/null @@ -1,669 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using AibisDream; -using AibisDream.SaveSystem; -using AibisDream.Utility; -using UnityEditor; -using UnityEngine; - -namespace AibisDream.SaveSystem.Editor -{ - /// - /// 存读档临时测试工具:触发存档、从槽位/测试归档读档、浏览 testsavs 历史。 - /// - public class SaveSystemTestWindow : EditorWindow - { - private const string MenuPath = "AIBIS/存档测试工具"; - private const string WindowTitle = "SaveSystem Test"; - - private Vector2 _scrollPosition; - private string _lastLog = string.Empty; - private int _restoreSlotIndex; - private int _manualSlotIndex = 1; - private bool _testArchiveEnabled; - private string _selectedArchiveEntryId; - private Texture2D _selectedThumbnail; - - private List _archiveEntries = new(); - private List _archiveEntriesSnapshot = new(); - private double _lastArchiveRefreshTime; - private bool _repaintQueued; - private List _slotOverviewLabels = new(); - private List _restoreLogSnapshot = new(); - private string _displaySelectedArchiveEntryId; - private Texture2D _displayThumbnail; - - private void RequestRepaint() - { - if (_repaintQueued) - { - return; - } - - _repaintQueued = true; - EditorApplication.delayCall += () => - { - _repaintQueued = false; - if (this != null) - { - Repaint(); - } - }; - } - - [MenuItem(MenuPath)] - public static void Open() - { - var window = GetWindow(WindowTitle); - window.minSize = new Vector2(480, 560); - } - - private void OnEnable() - { - _testArchiveEnabled = EditorPrefs.GetBool(TestSaveArchive.EditorPrefsEnabledKey, true); - RefreshArchiveEntries(requestRepaint: false); - EditorApplication.update += OnEditorUpdate; - EditorApplication.playModeStateChanged += OnPlayModeStateChanged; - } - - private void OnDisable() - { - EditorApplication.update -= OnEditorUpdate; - EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; - ReleaseThumbnail(); - } - - private void OnEditorUpdate() - { - if (!Application.isPlaying) - { - return; - } - - if (EditorApplication.timeSinceStartup - _lastArchiveRefreshTime < 1.0) - { - return; - } - - RefreshArchiveEntries(requestRepaint: true); - } - - private void OnPlayModeStateChanged(PlayModeStateChange state) - { - if (state == PlayModeStateChange.EnteredPlayMode || state == PlayModeStateChange.ExitingPlayMode) - { - RefreshArchiveEntries(); - } - } - - private void OnGUI() - { - if (Event.current.type == EventType.Layout) - { - SnapshotDynamicGuiData(); - } - - _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); - - DrawHeader(); - GUILayout.Space(8); - DrawTestArchiveSettings(); - GUILayout.Space(8); - DrawSaveActions(); - GUILayout.Space(8); - DrawSlotRestoreSection(); - GUILayout.Space(8); - DrawTestArchiveSection(); - GUILayout.Space(8); - DrawRestoreLogSection(); - GUILayout.Space(8); - DrawUtilitySection(); - GUILayout.Space(8); - DrawLogSection(); - - EditorGUILayout.EndScrollView(); - } - - /// - /// 在 Layout/Repaint 之前快照可变列表,避免同一 GUI 帧内控件数量不一致。 - /// - private void SnapshotDynamicGuiData() - { - _displaySelectedArchiveEntryId = _selectedArchiveEntryId; - _displayThumbnail = _selectedThumbnail; - - _archiveEntriesSnapshot.Clear(); - for (var i = 0; i < _archiveEntries.Count; i++) - { - _archiveEntriesSnapshot.Add(_archiveEntries[i]); - } - - _slotOverviewLabels.Clear(); - foreach (var vm in SlotManager.GetSlotViewModels()) - { - if (vm.IsEmpty) - { - continue; - } - - var meta = SlotManager.LoadMeta(vm.SlotIndex); - _slotOverviewLabels.Add( - $"slot_{vm.SlotIndex}" + - (vm.IsAutoSlot ? " [自动]" : "") + - $" | {vm.SavedAt} | {meta?.nodeName ?? "—"} | {vm.SceneName}"); - } - - _restoreLogSnapshot.Clear(); - var restoreLog = SaveRestoreOrchestrator.LastRestoreLog; - for (var i = 0; i < restoreLog.Count; i++) - { - _restoreLogSnapshot.Add(restoreLog[i]); - } - } - - private void DrawHeader() - { - EditorGUILayout.LabelField("存读档测试工具", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "用于 Play Mode 下验证自动存档、槽位读档与 testsavs 测试归档。\n" + - "正式存档目录: demo_saves/ | 测试归档: testsavs/(每次自动存档独立子文件夹)", - MessageType.Info); - - if (!Application.isPlaying) - { - EditorGUILayout.HelpBox("存档/读档操作需要在 Play Mode 下执行。", MessageType.Warning); - } - - EditorGUILayout.LabelField( - $"状态: IsRestoring={SaveRestoreOrchestrator.IsRestoring}, " + - $"SuppressAutoSave={SaveRestoreOrchestrator.IsAutoSaveSuppressed}"); - } - - private void DrawTestArchiveSettings() - { - EditorGUILayout.LabelField("测试存档模式", EditorStyles.boldLabel); - - EditorGUI.BeginChangeCheck(); - _testArchiveEnabled = EditorGUILayout.Toggle("启用测试存档模式", _testArchiveEnabled); - if (EditorGUI.EndChangeCheck()) - { - EditorPrefs.SetBool(TestSaveArchive.EditorPrefsEnabledKey, _testArchiveEnabled); - } - - EditorGUILayout.LabelField($"testsavs 路径: {ConstRef.TestAutoSaveArchivePath}", EditorStyles.miniLabel); - EditorGUILayout.HelpBox( - "开启后:每次 AutoSave 仅在 testsavs 下新建一份独立存档,不写入、不覆盖 demo_saves/slot_0。\n" + - "存多少次就保留多少份;关闭后恢复正式行为(仅覆盖 slot_0)。", - MessageType.None); - } - - private void DrawSaveActions() - { - EditorGUILayout.LabelField("存档", EditorStyles.boldLabel); - - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("触发自动存档 (完整流程)")) - { - TriggerAutoSave(); - } - - if (GUILayout.Button("仅 Capture (不写盘)")) - { - CaptureInMemory(); - } - } - - if (GUILayout.Button("手动档: 复制 slot_0 → 上方指定槽位")) - { - CopyAutoToManual(); - } - } - - private void DrawSlotRestoreSection() - { - EditorGUILayout.LabelField("从正式槽位读档", EditorStyles.boldLabel); - EditorGUILayout.LabelField($"demo_saves 路径: {ConstRef.SaveFilePath}", EditorStyles.miniLabel); - - var latest = SlotManager.GetLatestSlotIndex(); - EditorGUILayout.LabelField($"最近槽位: {(latest.HasValue ? $"slot_{latest.Value}" : "无")}"); - - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("读档: Latest")) - { - RestoreLatestSlot(); - } - - if (GUILayout.Button("读档: slot_0")) - { - RestoreSlot(SlotIndex.Auto); - } - } - - _restoreSlotIndex = EditorGUILayout.IntSlider("槽位", _restoreSlotIndex, 0, SlotIndex.ManualEnd); - if (GUILayout.Button($"读档: slot_{_restoreSlotIndex}")) - { - RestoreSlot(_restoreSlotIndex); - } - - GUILayout.Space(4); - _manualSlotIndex = EditorGUILayout.IntSlider( - "复制到手动槽位", - _manualSlotIndex, - SlotIndex.ManualStart, - SlotIndex.ManualEnd); - - DrawSlotBriefOverview(); - } - - private void DrawSlotBriefOverview() - { - foreach (var label in _slotOverviewLabels) - { - EditorGUILayout.LabelField(label, EditorStyles.miniLabel); - } - } - - private void DrawTestArchiveSection() - { - EditorGUILayout.LabelField("testsavs 测试归档", EditorStyles.boldLabel); - - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("刷新列表")) - { - RefreshArchiveEntries(); - } - - if (GUILayout.Button("清空 testsavs")) - { - ClearTestArchives(); - } - - if (GUILayout.Button("打开 testsavs 文件夹")) - { - OpenDirectory(ConstRef.TestAutoSaveArchivePath); - } - } - - EditorGUILayout.LabelField($"共 {_archiveEntriesSnapshot.Count} 条", EditorStyles.miniLabel); - - if (_archiveEntriesSnapshot.Count == 0) - { - EditorGUILayout.HelpBox( - "暂无测试归档。开启「testsavs 独立归档」后触发自动存档,或从正式 slot_0 手动归档。", - MessageType.None); - } - else - { - using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) - { - foreach (var entry in _archiveEntriesSnapshot) - { - DrawArchiveEntry(entry); - } - } - } - - if (GUILayout.Button("将当前 slot_0 手动归档到 testsavs")) - { - ArchiveCurrentAutoSlot(); - } - } - - private void DrawArchiveEntry(TestSaveArchiveEntry entry) - { - var isSelected = _displaySelectedArchiveEntryId == entry.EntryId; - var boxStyle = isSelected ? EditorStyles.helpBox : EditorStyles.textArea; - - using (new EditorGUILayout.VerticalScope(boxStyle)) - { - EditorGUILayout.LabelField(entry.EntryId, EditorStyles.boldLabel); - EditorGUILayout.LabelField( - $"时间: {entry.Meta?.savedAt ?? "—"} | 节点: {entry.Meta?.nodeName ?? "—"}", - EditorStyles.miniLabel); - EditorGUILayout.LabelField( - $"场景: {entry.Meta?.sceneName ?? "—"} | SO: {entry.Meta?.sceneSoName ?? "—"}", - EditorStyles.miniLabel); - - if (isSelected) - { - var maxWidth = EditorGUIUtility.currentViewWidth - 48f; - var height = 120f; - if (_displayThumbnail != null) - { - height = Mathf.Min(120f, _displayThumbnail.height * maxWidth / _displayThumbnail.width); - GUILayout.Label(_displayThumbnail, GUILayout.Width(maxWidth), GUILayout.Height(height)); - } - else - { - EditorGUILayout.LabelField( - "(无缩略图)", - EditorStyles.miniLabel, - GUILayout.Width(maxWidth), - GUILayout.Height(height)); - } - } - - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("选中")) - { - SelectArchiveEntry(entry); - } - - if (GUILayout.Button("读档")) - { - RestoreFromArchive(entry); - } - - if (GUILayout.Button("删除")) - { - DeleteArchiveEntry(entry); - } - } - } - - GUILayout.Space(4); - } - - private void DrawRestoreLogSection() - { - EditorGUILayout.LabelField("读档 Pipeline 日志", EditorStyles.boldLabel); - - if (_restoreLogSnapshot.Count == 0) - { - EditorGUILayout.LabelField("(暂无)", EditorStyles.miniLabel); - return; - } - - foreach (var line in _restoreLogSnapshot) - { - EditorGUILayout.LabelField(line, EditorStyles.wordWrappedMiniLabel); - } - } - - private void DrawUtilitySection() - { - EditorGUILayout.LabelField("工具", EditorStyles.boldLabel); - - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("打开 demo_saves 文件夹")) - { - OpenDirectory(ConstRef.SaveFilePath); - } - - if (GUILayout.Button("打开存档验证工具")) - { - SaveSystemValidationWindow.Open(); - } - } - } - - private void DrawLogSection() - { - EditorGUILayout.LabelField("操作结果", EditorStyles.boldLabel); - EditorGUILayout.SelectableLabel(_lastLog, EditorStyles.textArea, GUILayout.MinHeight(80)); - } - - private void RefreshArchiveEntries(bool requestRepaint = true) - { - var newEntries = TestSaveArchive.ListEntries(); - var entriesChanged = !ArchiveEntriesEqual(newEntries, _archiveEntries); - _archiveEntries = newEntries; - _lastArchiveRefreshTime = EditorApplication.timeSinceStartup; - - if (!string.IsNullOrEmpty(_selectedArchiveEntryId) && - !_archiveEntries.Exists(e => e.EntryId == _selectedArchiveEntryId)) - { - _selectedArchiveEntryId = null; - ReleaseThumbnail(); - entriesChanged = true; - } - - if (requestRepaint && entriesChanged) - { - RequestRepaint(); - } - } - - private static bool ArchiveEntriesEqual(List a, List b) - { - if (a.Count != b.Count) - { - return false; - } - - for (var i = 0; i < a.Count; i++) - { - if (a[i].EntryId != b[i].EntryId) - { - return false; - } - } - - return true; - } - - private void SelectArchiveEntry(TestSaveArchiveEntry entry) - { - _selectedArchiveEntryId = entry.EntryId; - ReleaseThumbnail(); - - if (!string.IsNullOrEmpty(entry.ThumbnailPath) && File.Exists(entry.ThumbnailPath)) - { - var bytes = File.ReadAllBytes(entry.ThumbnailPath); - _selectedThumbnail = new Texture2D(2, 2); - _selectedThumbnail.LoadImage(bytes); - } - - RequestRepaint(); - } - - private void ReleaseThumbnail() - { - if (_selectedThumbnail != null) - { - DestroyImmediate(_selectedThumbnail); - _selectedThumbnail = null; - } - } - - private void TriggerAutoSave() - { - if (!Application.isPlaying) - { - Log("需要在 Play Mode 下执行。"); - return; - } - - try - { - if (!SavePointEvaluator.CanAutoSave(out var reason)) - { - Log($"CanAutoSave 拒绝: {reason}"); - return; - } - - var routine = SaveRestoreOrchestrator.AutoSaveRoutine(); - while (routine.MoveNext()) - { - } - - RefreshArchiveEntries(); - Log(TestSaveArchive.IsEnabled - ? "测试存档模式:已写入 testsavs(未覆盖 slot_0)。" - : "正式模式:已写入 slot_0。"); - } - catch (Exception ex) - { - Log($"自动存档失败: {ex}"); - } - } - - private void CaptureInMemory() - { - try - { - if (YarnVariableStorage.Instance == null) - { - Log("YarnVariableStorage 未初始化。"); - return; - } - - var snapshot = SnapshotService.Capture(); - Log($"Capture 成功: 场景={snapshot.scene?.sceneName}, 节点={snapshot.anchor?.nodeName}"); - } - catch (Exception ex) - { - Log($"Capture 失败: {ex.Message}"); - } - } - - private void CopyAutoToManual() - { - if (!SlotDirectory.Exists(SlotIndex.Auto)) - { - Log("slot_0 不存在。"); - return; - } - - SlotManager.CopyAutoToManual(_manualSlotIndex); - Log($"已复制到 slot_{_manualSlotIndex}。"); - } - - private void RestoreLatestSlot() - { - var latest = SlotManager.GetLatestSlotIndex(); - if (!latest.HasValue) - { - Log("没有 latest_slot。"); - return; - } - - RestoreSlot(latest.Value); - } - - private void RestoreSlot(int slotIndex) - { - if (!Application.isPlaying) - { - Log("读档需要在 Play Mode 下执行。"); - return; - } - - if (GameManager.Instance == null) - { - Log("GameManager 未初始化。"); - return; - } - - if (!SlotDirectory.Exists(slotIndex)) - { - Log($"slot_{slotIndex} 不存在。"); - return; - } - - GameManager.Instance.TryRestoreSlot(slotIndex); - Log($"已启动读档 slot_{slotIndex}。"); - } - - private void RestoreFromArchive(TestSaveArchiveEntry entry) - { - if (!Application.isPlaying) - { - Log("读档需要在 Play Mode 下执行。"); - return; - } - - if (GameManager.Instance == null) - { - Log("GameManager 未初始化。"); - return; - } - - GameManager.Instance.TryRestoreFile( - entry.SnapshotPath, - RestoreOptions.DevJump); - Log($"已启动读档: {entry.EntryId}"); - } - - private void ArchiveCurrentAutoSlot() - { - var snapshot = SlotManager.LoadSnapshot(SlotIndex.Auto); - if (snapshot == null) - { - Log("slot_0 无快照。"); - return; - } - - var thumbnail = SlotManager.LoadThumbnail(SlotIndex.Auto); - var entryId = TestSaveArchive.Archive(snapshot, thumbnail); - RefreshArchiveEntries(); - Log($"已手动归档: {entryId}"); - } - - private void DeleteArchiveEntry(TestSaveArchiveEntry entry) - { - var entryId = entry.EntryId; - EditorApplication.delayCall += () => ConfirmDeleteArchiveEntry(entryId); - } - - private void ConfirmDeleteArchiveEntry(string entryId) - { - if (!EditorUtility.DisplayDialog("删除测试归档", $"删除 {entryId}?", "删除", "取消")) - { - return; - } - - TestSaveArchive.DeleteEntry(entryId); - if (_selectedArchiveEntryId == entryId) - { - _selectedArchiveEntryId = null; - ReleaseThumbnail(); - } - - RefreshArchiveEntries(); - Log($"已删除: {entryId}"); - } - - private void ClearTestArchives() - { - EditorApplication.delayCall += ConfirmClearTestArchives; - } - - private void ConfirmClearTestArchives() - { - if (!EditorUtility.DisplayDialog("清空 testsavs", "删除所有测试归档?不可撤销。", "删除", "取消")) - { - return; - } - - TestSaveArchive.ClearAll(); - _selectedArchiveEntryId = null; - ReleaseThumbnail(); - RefreshArchiveEntries(); - Log("已清空 testsavs。"); - } - - private static void OpenDirectory(string path) - { - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - - EditorUtility.RevealInFinder(path); - } - - private void Log(string message) - { - _lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}"; - Debug.Log($"[SaveSystemTest] {message}"); - RequestRepaint(); - } - } -} diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs.meta b/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs.meta deleted file mode 100644 index 8e6df22af..000000000 --- a/Assets/Editor/SaveSystemValidation/SaveSystemTestWindow.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 27c49345ec3a4e849af3f9da7045f7a3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs deleted file mode 100644 index b635dd98d..000000000 --- a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs +++ /dev/null @@ -1,704 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using AibisDream; -using AibisDream.SaveSystem; -using AibisDream.Utility; -using Newtonsoft.Json; -using UnityEditor; -using UnityEngine; - -namespace AibisDream.SaveSystem.Editor -{ - /// - /// 存档系统 P1/P2/P3 存盘行为验证工具。 - /// 不验证读档(P4),不验证复杂 tags 边界(P3 后续)。 - /// - public class SaveSystemValidationWindow : EditorWindow - { - private const string MenuPath = "AIBIS/存档系统验证工具"; - private const string WindowTitle = "SaveSystem Validator"; - - private const float SaveHistoryBoxMinHeight = 400f; - - private Vector2 _scrollPosition; - private string _lastLog = string.Empty; - private int _manualSlotIndex = 1; - private int _deleteSlotIndex = 1; - private string _testNodeName = "SomeNode"; - private string _testTags = ""; - private int _restoreSlotIndex; - - private readonly List _saveHistory = new(); - private string _lastObservedSavedAt; - - /// 运行时自动存档历史条目,仅内存缓存。 - private class SaveHistoryEntry - { - public string SavedAt; - public string NodeName; - public string SceneName; - } - - [MenuItem(MenuPath)] - public static void Open() - { - var window = GetWindow(WindowTitle); - window.minSize = new Vector2(420, 640); - } - - private void OnEnable() - { - EditorApplication.update += OnEditorUpdate; - EditorApplication.playModeStateChanged += OnPlayModeStateChanged; - InitializeLastObservedSavedAt(); - } - - private void OnDisable() - { - EditorApplication.update -= OnEditorUpdate; - EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; - } - - private void OnPlayModeStateChanged(PlayModeStateChange state) - { - if (state == PlayModeStateChange.EnteredPlayMode) - { - _saveHistory.Clear(); - InitializeLastObservedSavedAt(); - TrySeedHistoryFromCurrentAutoSlot(); - Repaint(); - return; - } - - if (state == PlayModeStateChange.ExitingPlayMode) - { - _saveHistory.Clear(); - _lastObservedSavedAt = null; - } - } - - private void TrySeedHistoryFromCurrentAutoSlot() - { - var meta = SlotManager.LoadMeta(SlotIndex.Auto); - if (meta == null) - { - return; - } - - _saveHistory.Add(CreateHistoryEntry(meta)); - _lastObservedSavedAt = meta.savedAt; - } - - private static SaveHistoryEntry CreateHistoryEntry(SlotMeta meta) - { - return new SaveHistoryEntry - { - SavedAt = meta.savedAt, - NodeName = string.IsNullOrEmpty(meta.nodeName) ? "(无节点)" : meta.nodeName, - SceneName = meta.sceneName - }; - } - - private void InitializeLastObservedSavedAt() - { - var meta = SlotManager.LoadMeta(SlotIndex.Auto); - _lastObservedSavedAt = meta?.savedAt; - } - - /// - /// 监听 slot_0 的 meta 变化,运行时每次自动存档都会记录到内存列表中。 - /// 纯 Editor 行为,不侵入运行时代码。 - /// - private void OnEditorUpdate() - { - if (!Application.isPlaying) - { - return; - } - - var meta = SlotManager.LoadMeta(SlotIndex.Auto); - if (meta == null) - { - return; - } - - if (!string.IsNullOrEmpty(_lastObservedSavedAt) && _lastObservedSavedAt == meta.savedAt) - { - return; - } - - _lastObservedSavedAt = meta.savedAt; - _saveHistory.Add(CreateHistoryEntry(meta)); - - Repaint(); - } - - private void OnGUI() - { - _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); - - DrawHeader(); - DrawSlotOverview(); - GUILayout.Space(12); - DrawSaveHistorySection(); - GUILayout.Space(12); - DrawDirectCaptureSection(); - GUILayout.Space(12); - DrawAutoSaveSimulationSection(); - GUILayout.Space(12); - DrawManualSlotSection(); - GUILayout.Space(12); - DrawRestoreSection(); - GUILayout.Space(12); - DrawSavePointEvaluationSection(); - GUILayout.Space(12); - DrawUtilitySection(); - GUILayout.Space(12); - DrawLogSection(); - - EditorGUILayout.EndScrollView(); - } - - #region Drawing - - private void DrawHeader() - { - EditorGUILayout.LabelField("存档系统存盘验证", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "本工具仅验证 P1/P2/P3 的存盘行为:快照捕获、槽位落盘、元数据、可存点判定框架。\n" + - "不验证读档(P4)和复杂 tags 边界(P3 后续)。\n" + - "涉及运行时单例(DialogController / GameManager)的操作需要在 Play Mode 下执行。", - MessageType.Info); - - if (!Application.isPlaying) - { - EditorGUILayout.HelpBox("当前不在 Play Mode。Capture / TryAutoSave 等依赖运行时单例的操作可能失败。", MessageType.Warning); - } - } - - private void DrawSlotOverview() - { - EditorGUILayout.LabelField("槽位概览", EditorStyles.boldLabel); - - var latest = SlotManager.GetLatestSlotIndex(); - EditorGUILayout.LabelField($"最近槽位: {(latest.HasValue ? latest.Value.ToString() : "无")}"); - EditorGUILayout.LabelField($"存档根目录: {ConstRef.SaveFilePath}"); - EditorGUILayout.LabelField( - $"读档状态: IsRestoring={SaveRestoreOrchestrator.IsRestoring}, " + - $"SuppressAutoSave={SaveRestoreOrchestrator.IsAutoSaveSuppressed}"); - - var viewModels = SlotManager.GetSlotViewModels(); - foreach (var vm in viewModels) - { - DrawSlotViewModel(vm, latest); - } - } - - private void DrawSlotViewModel(SlotViewModel vm, int? latest) - { - var isLatest = latest.HasValue && latest.Value == vm.SlotIndex; - var nodeName = SlotManager.LoadMeta(vm.SlotIndex)?.nodeName; - var label = vm.IsEmpty - ? $"[{vm.SlotIndex}] (空)" - : $"[{vm.SlotIndex}] {(vm.IsAutoSlot ? "[自动档]" : "")} {(isLatest ? "[最新]" : "")}\n" + - $" 场景: {vm.SceneName}\n" + - $" SO: {vm.SceneSoName}\n" + - $" 节点: {nodeName}\n" + - $" 时间: {vm.SavedAt}\n" + - $" 缩略图: {(string.IsNullOrEmpty(vm.ThumbnailPath) ? "无" : Path.GetFileName(vm.ThumbnailPath))}"; - - EditorGUILayout.HelpBox(label, vm.IsEmpty ? MessageType.None : MessageType.Info); - } - - private void DrawDirectCaptureSection() - { - EditorGUILayout.LabelField("直接捕获快照", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "调用 SnapshotService.Capture() 组装快照,并在内存中检查结构。\n" + - "此操作不写盘,不经过 SavePointEvaluator。", - MessageType.None); - - if (GUILayout.Button("Capture Snapshot (内存)")) - { - CaptureSnapshotInMemory(); - } - } - - private void DrawAutoSaveSimulationSection() - { - EditorGUILayout.LabelField("模拟自动存档", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "调用 SaveRestoreOrchestrator.TryAutoSave(),走完整的判定+捕获+落盘流程。\n" + - "需要在 Play Mode 下且有 DialogController 运行时实例。", - MessageType.None); - - if (GUILayout.Button("TryAutoSave (完整流程)")) - { - TryAutoSaveFullFlow(); - } - } - - private void DrawSaveHistorySection() - { - EditorGUILayout.LabelField("自动存档历史 (运行时)", EditorStyles.boldLabel); - - using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.MinHeight(SaveHistoryBoxMinHeight))) - { - if (!Application.isPlaying) - { - EditorGUILayout.HelpBox( - "进入 Play Mode 后,每次 slot_0 自动存档都会在此记录节点名与时间。\n" + - "数据仅保存在当前窗口内存中,退出 Play Mode 后清空。", - MessageType.Info); - } - else if (_saveHistory.Count == 0) - { - EditorGUILayout.LabelField("等待自动存档…", EditorStyles.wordWrappedLabel); - EditorGUILayout.LabelField( - "触发 content / hub / linear 节点,或点击下方 TryAutoSave。", - EditorStyles.miniLabel); - } - else - { - EditorGUILayout.LabelField($"共 {_saveHistory.Count} 条记录", EditorStyles.miniLabel); - - for (var i = 0; i < _saveHistory.Count; i++) - { - var entry = _saveHistory[i]; - EditorGUILayout.LabelField( - $"{i + 1}. [{entry.SavedAt}] {entry.NodeName}", - EditorStyles.wordWrappedLabel); - EditorGUILayout.LabelField( - $" 场景: {entry.SceneName ?? "N/A"}", - EditorStyles.miniLabel); - } - - GUILayout.FlexibleSpace(); - - if (GUILayout.Button("清空历史记录")) - { - _saveHistory.Clear(); - } - } - } - } - - private void DrawManualSlotSection() - { - EditorGUILayout.LabelField("手动档操作", EditorStyles.boldLabel); - - _manualSlotIndex = EditorGUILayout.IntSlider("复制到槽位", _manualSlotIndex, 1, SlotIndex.ManualEnd); - if (GUILayout.Button("Copy Auto → Manual")) - { - CopyAutoToManual(_manualSlotIndex); - } - - GUILayout.Space(8); - - _deleteSlotIndex = EditorGUILayout.IntSlider("删除槽位", _deleteSlotIndex, 0, SlotIndex.ManualEnd); - if (GUILayout.Button("Delete Slot")) - { - DeleteSlot(_deleteSlotIndex); - } - - GUILayout.Space(8); - - if (GUILayout.Button("Clear All Saves")) - { - ClearAllSaves(); - } - } - - private void DrawSavePointEvaluationSection() - { - EditorGUILayout.LabelField("可存点判定框架检查", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "手动设置节点名和 tags,模拟 OnNodeStart 自动档判定(含 DetourResume)。\n" + - "InProgress 来自运行时 DialogController;全局门控按实际 Play Mode 状态取值。", - MessageType.None); - - _testNodeName = EditorGUILayout.TextField("节点名", _testNodeName); - _testTags = EditorGUILayout.TextField("Tags (逗号分隔)", _testTags); - - if (GUILayout.Button("Evaluate CanAutoSave")) - { - EvaluateSavePoint(); - } - } - - private void DrawRestoreSection() - { - EditorGUILayout.LabelField("读档验证 (P4)", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "触发 GameManager.TryRestoreSlot,验证预检、Phase + Barrier 编排、自动存档抑制与 Provider 还原日志。\n" + - "需要在 Play Mode 下执行;不代表正式玩家 UI。", - MessageType.None); - - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("Restore Latest")) - { - RestoreLatestSlot(); - } - - if (GUILayout.Button("Restore slot_0")) - { - RestoreSlot(SlotIndex.Auto); - } - } - - _restoreSlotIndex = EditorGUILayout.IntSlider("Restore 槽位", _restoreSlotIndex, 0, SlotIndex.ManualEnd); - if (GUILayout.Button($"Restore slot_{_restoreSlotIndex}")) - { - RestoreSlot(_restoreSlotIndex); - } - - var restoreLog = SaveRestoreOrchestrator.LastRestoreLog; - if (restoreLog.Count > 0) - { - EditorGUILayout.LabelField("Restore Pipeline Log", EditorStyles.boldLabel); - foreach (var line in restoreLog) - { - EditorGUILayout.LabelField(line, EditorStyles.wordWrappedMiniLabel); - } - } - } - - private void DrawUtilitySection() - { - EditorGUILayout.LabelField("工具", EditorStyles.boldLabel); - - if (GUILayout.Button("打开存档目录")) - { - OpenSaveDirectory(); - } - - if (GUILayout.Button("刷新")) - { - Repaint(); - Log("已刷新。"); - } - } - - private void DrawLogSection() - { - GUILayout.Space(12); - EditorGUILayout.LabelField("最后结果", EditorStyles.boldLabel); - EditorGUILayout.SelectableLabel(_lastLog, EditorStyles.textArea, GUILayout.MinHeight(120)); - } - - #endregion - - #region Actions - - private void CaptureSnapshotInMemory() - { - try - { - var storage = YarnVariableStorage.Instance; - if (storage == null) - { - Log("YarnVariableStorage 未初始化,尝试直接 new SaveSnapshot 仅做结构演示。"); - var demo = new SaveSnapshot - { - gameVersion = Application.version, - savedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), - scene = new SceneSnapshotDto { sceneName = "DemoScene" }, - anchor = new AnchorSnapshot { nodeName = "DemoNode" }, - yarnVariables = new YarnVariablesSnapshot() - }; - PrintSnapshotSummary(demo, "演示快照(无运行时)"); - return; - } - - var snapshot = SnapshotService.Capture(); - PrintSnapshotSummary(snapshot, "内存快照"); - } - catch (Exception ex) - { - Log($"Capture 失败: {ex}"); - } - } - - private void TryAutoSaveFullFlow() - { - if (!Application.isPlaying) - { - Log("TryAutoSave 需要在 Play Mode 下执行。"); - return; - } - - try - { - var beforeLatest = SlotManager.GetLatestSlotIndex(); - var beforeSnapshot = SlotManager.LoadSnapshot(0); - - if (!SavePointEvaluator.CanAutoSave(out var rejectReason)) - { - Log($"CanAutoSave 拒绝: {rejectReason}"); - return; - } - - var routine = SaveRestoreOrchestrator.AutoSaveRoutine(); - while (routine.MoveNext()) - { - } - - var afterSnapshot = SlotManager.LoadSnapshot(0); - var afterLatest = SlotManager.GetLatestSlotIndex(); - - var summary = string.Empty; - if (afterSnapshot == null) - { - summary = "无快照写入(可能被 SavePointEvaluator 拒绝)。"; - } - else - { - summary = $"落盘成功。\n" + - $"场景: {afterSnapshot.scene?.sceneName}\n" + - $"节点: {afterSnapshot.anchor?.nodeName}\n" + - $"SO: {afterSnapshot.anchor?.sceneSoName}\n" + - $"YarnProject: {afterSnapshot.anchor?.yarnProjectId}\n" + - $"Sections: {string.Join(", ", afterSnapshot.sections?.Keys ?? Enumerable.Empty())}\n" + - $"latest_slot: {(afterLatest.HasValue ? afterLatest.Value.ToString() : "无")}"; - } - - Log(summary); - } - catch (Exception ex) - { - Log($"TryAutoSave 失败: {ex}"); - } - } - - private void CopyAutoToManual(int slotIndex) - { - try - { - if (!SlotDirectory.Exists(SlotIndex.Auto)) - { - Log("自动档不存在,无法复制手动档。"); - return; - } - - SlotManager.CopyAutoToManual(slotIndex); - Log($"已复制自动档到 slot_{slotIndex}。"); - } - catch (Exception ex) - { - Log($"复制失败: {ex.Message}"); - } - } - - private void DeleteSlot(int slotIndex) - { - try - { - SlotManager.DeleteSlot(slotIndex); - Log($"已删除/清空 slot_{slotIndex}。"); - } - catch (Exception ex) - { - Log($"删除失败: {ex.Message}"); - } - } - - private void ClearAllSaves() - { - if (!EditorUtility.DisplayDialog("确认", "删除所有存档槽位?此操作不可撤销。", "删除", "取消")) - { - return; - } - - try - { - for (var i = SlotIndex.Auto; i <= SlotIndex.ManualEnd; i++) - { - SlotManager.DeleteSlot(i); - } - - var latestPath = SlotDirectory.GetLatestSlotIndexPath(); - if (File.Exists(latestPath)) - { - File.Delete(latestPath); - } - - Log("已清空所有槽位和 latest_slot 记录。"); - } - catch (Exception ex) - { - Log($"清空失败: {ex.Message}"); - } - } - - private void EvaluateSavePoint() - { - if (!Application.isPlaying) - { - Log("SavePointEvaluator 需要运行时状态(IsRestoring / GameManager pause)。请在 Play Mode 下执行。"); - return; - } - - try - { - var projectId = DialogController.Instance?.DialogueRunner?.YarnProject?.name; - var tags = ParseTestTags(_testTags); - var inProgress = SavePointEvaluator.InProgressNodeNames; - - if (SaveRestoreOrchestrator.IsAutoSaveSuppressed) - { - Log("全局门控: AutoSaveSuppressed"); - return; - } - - if (SaveRestoreOrchestrator.IsRestoring) - { - Log("全局门控: Restoring(读档重进走 OnRestoreEnterNode,不写盘)"); - return; - } - - if (GameManager.Instance != null && GameManager.Session.IsPaused) - { - Log("全局门控: GamePaused"); - return; - } - - var detourResume = !string.IsNullOrEmpty(_testNodeName) - && inProgress.Contains(_testNodeName); - var tagOk = SavePointEvaluator.EvaluateNodeTagsForAutoSave( - _testNodeName, tags, out var tagReason); - var wouldSave = !detourResume && tagOk; - - Log( - $"Project = {projectId ?? "(null)"}\n" + - $"InProgress = [{string.Join(", ", inProgress)}]\n" + - $"模拟节点 = {_testNodeName}, tags = [{string.Join(", ", tags)}]\n" + - $"DetourResume = {detourResume}\n" + - $"Tag 判定 = {tagOk}, TagReason = {tagReason}\n" + - $"OnNodeStart 会自动存 = {wouldSave}\n" + - $"(DialogController 当前上下文 CanAutoSave = {SavePointEvaluator.CanAutoSave(out var ctxReason)}, Reason = {ctxReason})"); - } - catch (Exception ex) - { - Log($"判定失败: {ex.Message}"); - } - } - - private static string[] ParseTestTags(string raw) - { - if (string.IsNullOrWhiteSpace(raw)) - { - return Array.Empty(); - } - - return raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(tag => tag.Trim()) - .Where(tag => tag.Length > 0) - .ToArray(); - } - - private void RestoreLatestSlot() - { - var latest = SlotManager.GetLatestSlotIndex(); - if (!latest.HasValue) - { - Log("没有 latest_slot,无法读档。"); - return; - } - - RestoreSlot(latest.Value); - } - - private void RestoreSlot(int slotIndex) - { - if (!Application.isPlaying) - { - Log("Restore 需要在 Play Mode 下执行。"); - return; - } - - if (GameManager.Instance == null) - { - Log("GameManager 未初始化,无法启动读档协程。"); - return; - } - - if (!SlotDirectory.Exists(slotIndex)) - { - Log($"slot_{slotIndex} 不存在,无法读档。"); - return; - } - - GameManager.Instance.TryRestoreSlot(slotIndex); - Log($"已启动 Restore slot_{slotIndex}。请观察 Restore Pipeline Log 与场景状态。"); - } - - private void OpenSaveDirectory() - { - var path = ConstRef.SaveFilePath; - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - - EditorUtility.RevealInFinder(path); - } - - #endregion - - #region Helpers - - private void PrintSnapshotSummary(SaveSnapshot snapshot, string label) - { - var json = SnapshotPersistence.Serialize(snapshot); - var lines = new List - { - $"=== {label} ===", - $"gameVersion: {snapshot.gameVersion}", - $"savedAt: {snapshot.savedAt}", - $"schemaVersion: {snapshot.schemaVersion}", - $"scene: {snapshot.scene?.sceneName}", - $"anchor.sceneSoName: {snapshot.anchor?.sceneSoName}", - $"anchor.yarnProjectId: {snapshot.anchor?.yarnProjectId}", - $"anchor.nodeName: {snapshot.anchor?.nodeName}", - $"yarnVariables.floats: {snapshot.yarnVariables?.floats?.Count ?? 0} entries", - $"yarnVariables.strings: {snapshot.yarnVariables?.strings?.Count ?? 0} entries", - $"yarnVariables.bools: {snapshot.yarnVariables?.bools?.Count ?? 0} entries", - $"sections: {(snapshot.sections != null ? string.Join(", ", snapshot.sections.Keys) : "null")}", - }; - - if (snapshot.sections != null) - { - AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.PunchTape); - AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Fix); - AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.FixPanel); - AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.BodyModule); - AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Eye); - } - - lines.Add("JSON preview (first 1500 chars):"); - lines.Add(json.Length > 1500 ? json.Substring(0, 1500) + "..." : json); - - Log(string.Join("\n", lines)); - } - - private static void AppendFixSectionHint(List lines, SaveSnapshot snapshot, string sectionId) - { - if (snapshot.sections.ContainsKey(sectionId)) - { - lines.Add($" [fix-related] {sectionId}: captured"); - } - } - - private void Log(string message) - { - _lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}"; - Debug.Log($"[SaveSystemValidator] {message}"); - Repaint(); - } - - #endregion - } -} diff --git a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs.meta b/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs.meta deleted file mode 100644 index c912867a3..000000000 --- a/Assets/Editor/SaveSystemValidation/SaveSystemValidationWindow.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9719ab6f265683c4484a170f6c4dc843 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Editor/UXML/AnimatorLinker/AnimatorClipLink.cs b/Assets/Editor/UXML/AnimatorLinker/AnimatorClipLink.cs deleted file mode 100644 index ffbf26d2e..000000000 --- a/Assets/Editor/UXML/AnimatorLinker/AnimatorClipLink.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using JetBrains.Annotations; -using UnityEditor; -using UnityEditor.Animations; -using UnityEditor.UIElements; -using UnityEngine; -using UnityEngine.UIElements; -using Object = UnityEngine.Object; - -namespace AibisDream.SystemEditor -{ - public class AnimatorClipLink : EditorWindow - { - [SerializeField] private VisualTreeAsset visualTreeAsset; - - private ObjectField _animatorField; - private Label _animatorMessage; - private ListView _stateInfoView; - - private TextField _clipsPathField; - private Label _clipsMessage; - private ListView _clipsNameList; - - private HelpBox _matchMessageBox; - - #region 部分缓存 - - private static AnimatorController _animatorCache; - private static string _clipsPathCache; - - private ChildAnimatorState[] _statesCache; - private string[] _clipsCache; - - private bool _isChecked; - private bool _allMatched; - private bool _isAllLoad; - - #endregion - - [MenuItem("Tools/AnimatorClipLink")] - public static void ShowExample() - { - AnimatorClipLink wnd = GetWindow(); - wnd.titleContent = new GUIContent("AnimatorClipLink"); - } - - public void CreateGUI() - { - // Each editor window contains a root VisualElement object - VisualElement root = rootVisualElement; - - // Instantiate UXML - root.Add(visualTreeAsset.Instantiate()); - - // 注册一些事件 - _animatorField = root.Q("animator-field"); - _animatorField.RegisterValueChangedCallback(OnAnimatorChanged); - _animatorMessage = root.Q