From ddc8c5e63635fed6bcf873b17a3825afc85846a8 Mon Sep 17 00:00:00 2001 From: CL <62653664+Chen-Luan@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:32:21 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E5=88=B6=E8=B0=B1=E5=99=A8?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E8=B0=B1=E9=9D=A2=E5=90=8E=E7=A6=81=E7=94=A8?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChartEditor/ChartEditorDataModule.cs | 8 +++ .../ChartEditor/ChartEditorSceneRoot.cs | 5 ++ .../ChartEditor/Command/CommandStack.cs | 49 +++++++++++++++++++ .../ChartEditor/View/MenuButtonsView.cs | 12 +++++ .../ViewModel/MenuButtonsViewModel.cs | 11 ++++- 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs index 50f26a3ca..a3427dead 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs @@ -3,6 +3,7 @@ using System; using CyanStars.Framework; using CyanStars.Gameplay.ChartEditor.Command; +using R3; namespace CyanStars.Gameplay.ChartEditor { @@ -10,6 +11,11 @@ public class ChartEditorDataModule : BaseDataModule { public CommandStack CommandStack { get; private set; } = null!; + /// + /// 是否存在未保存数据(未进入制谱器时为 null,使用前请先判空) + /// + public ReadOnlyReactiveProperty? HasUnsavedChanges { get; private set; } + public override void OnInit() { @@ -18,6 +24,7 @@ public override void OnInit() public void OnEnterChartEditorProcedure(CommandStack targetCommandStack) { CommandStack = targetCommandStack; + HasUnsavedChanges = targetCommandStack.HasUnsavedChanges; } public void OnExitChartEditorProcedure() @@ -26,6 +33,7 @@ public void OnExitChartEditorProcedure() throw new Exception("未找到 CommandStack,未加载过或已经卸载?请检查业务逻辑。"); CommandStack = null; + HasUnsavedChanges = null; } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs index a663cd391..f4dd1b323 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs @@ -105,6 +105,11 @@ public void InitSceneRoot() chartMetadataIndex = (int)chartModule.SelectedChartIndex; } + // 新建谱面从未保存过,标记为有未保存数据 + // 判断条件与上方"新建谱面"分支一致:新建谱包或新建谱面 + if (chartModule.SelectedRuntimeChartPack is null || chartModule.ChartData is null) + commandStack.MarkDirty(); + mvvmBindManager.StartBind( workspacePath, chartMetadataIndex, diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs index 88587bcb6..bcef54964 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; +using R3; using UnityEngine; namespace CyanStars.Gameplay.ChartEditor.Command @@ -17,6 +18,18 @@ public class CommandStack : MonoBehaviour // -1 表示没有任何命令被执行(初始状态或全部撤销) private int currentCommandIndex = -1; + // 干净边界:最近一次成功保存时的命令索引,与 currentCommandIndex 相等即代表无未保存数据 + // 若边界落在被丢弃的历史中(保存后撤销再执行新命令),数据不可能再与磁盘一致, + // 此时置为 int.MinValue 使其不可达,直到下一次保存 + private int cleanBoundaryIndex = -1; + + private readonly ReactiveProperty hasUnsavedChanges = new ReactiveProperty(false); + + /// + /// 是否存在未保存数据(订阅时立即推送当前值) + /// + public ReadOnlyReactiveProperty HasUnsavedChanges => hasUnsavedChanges; + /// /// 执行新命令 /// @@ -36,6 +49,12 @@ public void ExecuteCommand(ICommand command) CommandHistory.Add(command); currentCommandIndex++; + // 若干净边界落在被丢弃的历史中,数据不可能再与磁盘一致 + if (cleanBoundaryIndex > currentCommandIndex) + cleanBoundaryIndex = int.MinValue; + + UpdateUnsavedState(); + // TODO: 可选添加最大历史记录限制,防止内存溢出 } @@ -53,6 +72,7 @@ public void Undo() CommandHistory[currentCommandIndex].Undo(); currentCommandIndex--; + UpdateUnsavedState(); } /// @@ -69,6 +89,7 @@ public void Redo() currentCommandIndex++; CommandHistory[currentCommandIndex].Execute(); + UpdateUnsavedState(); } /// @@ -78,6 +99,34 @@ public void Clear() { CommandHistory.Clear(); currentCommandIndex = -1; + cleanBoundaryIndex = -1; + UpdateUnsavedState(); + } + + /// + /// 标记当前数据为已保存(保存成功后调用) + /// + public void MarkSaved() + { + cleanBoundaryIndex = currentCommandIndex; + UpdateUnsavedState(); + } + + /// + /// 强制标记为有未保存数据(新建谱面等不经过命令栈的数据修改使用) + /// + public void MarkDirty() + { + cleanBoundaryIndex = int.MinValue; + UpdateUnsavedState(); + } + + /// + /// 根据命令位置与干净边界是否相等来重算未保存状态 + /// + private void UpdateUnsavedState() + { + hasUnsavedChanges.Value = currentCommandIndex != cleanBoundaryIndex; } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/MenuButtonsView.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/MenuButtonsView.cs index 39d15b210..768c2ab2f 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/MenuButtonsView.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/MenuButtonsView.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using CyanStars.Gameplay.ChartEditor.ViewModel; +using CyanStars.Utils.SelectableUI; using DG.Tweening; using R3; using UnityEngine; @@ -76,6 +77,7 @@ public class MenuButtonsView : BaseView private readonly List ShortcutListeners = new(); private Canvas functionCanvas = null!; + private SelectableStateObserver? saveButtonSelectableStateObserver; private void OnEnable() @@ -90,6 +92,7 @@ public override void Bind(MenuButtonsViewModel targetViewModel) base.Bind(targetViewModel); functionCanvas = functionCanvasGroup.GetComponent(); + saveButtonSelectableStateObserver = saveButton.GetComponent(); FunctionCanvasVisibility .Subscribe(isVisible => @@ -132,6 +135,15 @@ public override void Bind(MenuButtonsViewModel targetViewModel) .OnClickAsObservable() .Subscribe(_ => OnSaveRequested()) .AddTo(this); + ViewModel.HasUnsavedChanges + .Subscribe(hasUnsavedChanges => + { + if (saveButtonSelectableStateObserver != null) + saveButtonSelectableStateObserver.SetInteractable(hasUnsavedChanges); + else + saveButton.interactable = hasUnsavedChanges; + }) + .AddTo(this); // testButton ... undoButton .OnClickAsObservable() diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs index 611ece4fe..8c6e01f13 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs @@ -31,14 +31,23 @@ public void Redo() base.CommandStack.Redo(); } + /// + /// 是否存在未保存数据(透传命令栈的状态) + /// + public ReadOnlyReactiveProperty HasUnsavedChanges => CommandStack.HasUnsavedChanges; + public void SaveFileToDisk() { - ChartEditorFileManager.SaveChartAndAssetsToDisk( + bool isSaveSuccess = ChartEditorFileManager.SaveChartAndAssetsToDisk( Model.WorkspacePath, Model.ChartMetaDataIndex, Model.ChartPackData.CurrentValue, Model.ChartData.CurrentValue ); + + // 只有保存成功才标记为已保存,失败保持未保存状态便于重试 + if (isSaveSuccess) + CommandStack.MarkSaved(); } } } From ace7526bd0b43edb171f21712bd5b7f64db268dd Mon Sep 17 00:00:00 2001 From: CL <62653664+Chen-Luan@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:16:15 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=E5=88=9B=E5=BB=BA=20DirtyServe?= =?UTF-8?q?r=20=E6=9D=A5=E7=AE=A1=E7=90=86=E4=BF=9D=E5=AD=98=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChartEditor/ChartEditorDataModule.cs | 9 -- .../ChartEditor/ChartEditorDirtyServer.cs | 82 +++++++++++++++++++ .../ChartEditorDirtyServer.cs.meta | 3 + .../ChartEditor/ChartEditorSceneRoot.cs | 11 +-- .../ChartEditor/Command/CommandStack.cs | 49 ----------- .../ChartEditor/Management/MvvmBindManager.cs | 31 ++++--- .../ViewModel/MenuButtonsViewModel.cs | 27 +++--- .../ViewModel/NoteAttributeViewModel.cs | 13 +++ 8 files changed, 134 insertions(+), 91 deletions(-) create mode 100644 Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs create mode 100644 Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs index a3427dead..54a3e176c 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDataModule.cs @@ -3,7 +3,6 @@ using System; using CyanStars.Framework; using CyanStars.Gameplay.ChartEditor.Command; -using R3; namespace CyanStars.Gameplay.ChartEditor { @@ -11,12 +10,6 @@ public class ChartEditorDataModule : BaseDataModule { public CommandStack CommandStack { get; private set; } = null!; - /// - /// 是否存在未保存数据(未进入制谱器时为 null,使用前请先判空) - /// - public ReadOnlyReactiveProperty? HasUnsavedChanges { get; private set; } - - public override void OnInit() { } @@ -24,7 +17,6 @@ public override void OnInit() public void OnEnterChartEditorProcedure(CommandStack targetCommandStack) { CommandStack = targetCommandStack; - HasUnsavedChanges = targetCommandStack.HasUnsavedChanges; } public void OnExitChartEditorProcedure() @@ -33,7 +25,6 @@ public void OnExitChartEditorProcedure() throw new Exception("未找到 CommandStack,未加载过或已经卸载?请检查业务逻辑。"); CommandStack = null; - HasUnsavedChanges = null; } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs new file mode 100644 index 000000000..07ac905db --- /dev/null +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs @@ -0,0 +1,82 @@ +#nullable enable + +using System; +using CyanStars.Gameplay.ChartEditor.Model; +using ObservableCollections; +using R3; + +namespace CyanStars.Gameplay.ChartEditor.Management +{ + /// + /// 制谱器"是否存在未保存数据"的服务 + /// + public class ChartEditorDirtyServer : IDisposable + { + private readonly CompositeDisposable Disposables = new CompositeDisposable(); + private readonly ReactiveProperty hasUnsavedChanges = new ReactiveProperty(false); + + /// + /// 是否存在未保存数据 + /// + public ReadOnlyReactiveProperty HasUnsavedChanges => hasUnsavedChanges; + + + /// + /// 绑定到制谱器 Model + /// + /// 制谱器 Model + /// 会话初始是否存在未保存数据(新建谱面为 true,加载已有谱面为 false) + public ChartEditorDirtyServer(ChartEditorModel model, bool initialHasUnsavedChanges) + { + hasUnsavedChanges.Value = initialHasUnsavedChanges; + + GetDataChangeSource(model) + .Subscribe(_ => hasUnsavedChanges.Value = true) + .AddTo(Disposables); + } + + /// + /// 标记为已保存(保存成功后调用) + /// + public void MarkSaved() => hasUnsavedChanges.Value = false; + + public void Dispose() => Disposables.Dispose(); + + + private static Observable GetDataChangeSource(ChartEditorModel model) + { + var cp = model.ChartPackData.CurrentValue; + var cd = model.ChartData.CurrentValue; + + return Observable.Merge( + cd.ReadyBeat.AsObservable().Skip(1).Select(_ => Unit.Default), + ToUnit(cd.SpeedGroupDatas), + ToUnit(cd.Notes), + ToUnit(cd.TrackDatas), + cp.DataVersion.AsObservable().Skip(1).Select(_ => Unit.Default), + cp.Title.AsObservable().Skip(1).Select(_ => Unit.Default), + cp.ChartPackInfo.AsObservable().Skip(1).Select(_ => Unit.Default), + cp.MusicPreviewStartBeat.AsObservable().Skip(1).Select(_ => Unit.Default), + cp.MusicPreviewEndBeat.AsObservable().Skip(1).Select(_ => Unit.Default), + cp.CoverFilePath.AsObservable().Skip(1).Select(_ => Unit.Default), + cp.CropStartPositionPercent.AsObservable().Skip(1).Select(_ => Unit.Default), + cp.CropHeightPercent.AsObservable().Skip(1).Select(_ => Unit.Default), + ToUnit(cp.MusicVersions), + ToUnit(cp.BpmGroup), + ToUnit(cp.ChartMetaDatas), + + // 列表子项修改 + model.BpmGroupDataChangedSubject.Select(_ => Unit.Default), + model.SelectedNoteDataChangedSubject.Select(_ => Unit.Default) + ); + } + + private static Observable ToUnit(ObservableList list) => + Observable.Merge( + list.ObserveAdd().Select(_ => Unit.Default), + list.ObserveRemove().Select(_ => Unit.Default), + list.ObserveReplace().Select(_ => Unit.Default), + list.ObserveReset().Select(_ => Unit.Default) + ); + } +} diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta new file mode 100644 index 000000000..b157256df --- /dev/null +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 826d8cdef01f571ed36a9629a66120cc +timeCreated: 1786377845 diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs index f4dd1b323..609222b8c 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorSceneRoot.cs @@ -61,6 +61,9 @@ public void InitSceneRoot() ChartPackData chartPackData; ChartData chartData; + // 是否为新建谱面 + bool isNewChart = false; + if (chartModule.SelectedRuntimeChartPack is null) { // 创建新谱包和谱面 @@ -77,6 +80,7 @@ public void InitSceneRoot() chartPackData = new ChartPackData(randomName, bpmGroup: bpmGroup, chartMetaDatas: new List { chartMetaData }); chartMetadataIndex = 0; + isNewChart = true; } else if (chartModule.ChartData is null) { @@ -92,6 +96,7 @@ public void InitSceneRoot() chartPackData.ChartMetaDatas.Add(chartMetaData); chartMetadataIndex = chartPackData.ChartMetaDatas.Count - 1; + isNewChart = true; } else { @@ -105,16 +110,12 @@ public void InitSceneRoot() chartMetadataIndex = (int)chartModule.SelectedChartIndex; } - // 新建谱面从未保存过,标记为有未保存数据 - // 判断条件与上方"新建谱面"分支一致:新建谱包或新建谱面 - if (chartModule.SelectedRuntimeChartPack is null || chartModule.ChartData is null) - commandStack.MarkDirty(); - mvvmBindManager.StartBind( workspacePath, chartMetadataIndex, chartPackData, chartData, + isNewChart, musicManager, noteAudioManager, shortcutManager, diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs index bcef54964..88587bcb6 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs @@ -1,7 +1,6 @@ #nullable enable using System.Collections.Generic; -using R3; using UnityEngine; namespace CyanStars.Gameplay.ChartEditor.Command @@ -18,18 +17,6 @@ public class CommandStack : MonoBehaviour // -1 表示没有任何命令被执行(初始状态或全部撤销) private int currentCommandIndex = -1; - // 干净边界:最近一次成功保存时的命令索引,与 currentCommandIndex 相等即代表无未保存数据 - // 若边界落在被丢弃的历史中(保存后撤销再执行新命令),数据不可能再与磁盘一致, - // 此时置为 int.MinValue 使其不可达,直到下一次保存 - private int cleanBoundaryIndex = -1; - - private readonly ReactiveProperty hasUnsavedChanges = new ReactiveProperty(false); - - /// - /// 是否存在未保存数据(订阅时立即推送当前值) - /// - public ReadOnlyReactiveProperty HasUnsavedChanges => hasUnsavedChanges; - /// /// 执行新命令 /// @@ -49,12 +36,6 @@ public void ExecuteCommand(ICommand command) CommandHistory.Add(command); currentCommandIndex++; - // 若干净边界落在被丢弃的历史中,数据不可能再与磁盘一致 - if (cleanBoundaryIndex > currentCommandIndex) - cleanBoundaryIndex = int.MinValue; - - UpdateUnsavedState(); - // TODO: 可选添加最大历史记录限制,防止内存溢出 } @@ -72,7 +53,6 @@ public void Undo() CommandHistory[currentCommandIndex].Undo(); currentCommandIndex--; - UpdateUnsavedState(); } /// @@ -89,7 +69,6 @@ public void Redo() currentCommandIndex++; CommandHistory[currentCommandIndex].Execute(); - UpdateUnsavedState(); } /// @@ -99,34 +78,6 @@ public void Clear() { CommandHistory.Clear(); currentCommandIndex = -1; - cleanBoundaryIndex = -1; - UpdateUnsavedState(); - } - - /// - /// 标记当前数据为已保存(保存成功后调用) - /// - public void MarkSaved() - { - cleanBoundaryIndex = currentCommandIndex; - UpdateUnsavedState(); - } - - /// - /// 强制标记为有未保存数据(新建谱面等不经过命令栈的数据修改使用) - /// - public void MarkDirty() - { - cleanBoundaryIndex = int.MinValue; - UpdateUnsavedState(); - } - - /// - /// 根据命令位置与干净边界是否相等来重算未保存状态 - /// - private void UpdateUnsavedState() - { - hasUnsavedChanges.Value = currentCommandIndex != cleanBoundaryIndex; } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs index e1c83f6d1..de3242c0e 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs @@ -14,6 +14,8 @@ public class MvvmBindManager : MonoBehaviour { private readonly CompositeDisposable Disposables = new CompositeDisposable(); + private ChartEditorDirtyServer dirtyServer = null!; + [SerializeField] private ToolbarView toolbarView = null!; @@ -62,18 +64,21 @@ public class MvvmBindManager : MonoBehaviour /// /// 注意:由于引用关系,制谱器会修改传入的谱包和谱面实例内的数据。请先深拷贝一个谱包和谱面,再调用制谱器初始化 public void StartBind(string workspacePath, - int chartMetadataIndex, - ChartPackData chartPackData, - ChartData chartData, - ChartEditorMusicManager musicManager, - ChartEditorNoteAudioManager chartEditorNoteAudioManager, - ShortcutManager shortcutManager, - ChartEditorPlayerPrefsManager playerPrefsManager) + int chartMetadataIndex, + ChartPackData chartPackData, + ChartData chartData, + bool initialHasUnsavedChanges, + ChartEditorMusicManager musicManager, + ChartEditorNoteAudioManager chartEditorNoteAudioManager, + ShortcutManager shortcutManager, + ChartEditorPlayerPrefsManager playerPrefsManager) { - // TODO: 为 Model 实现 IDispose,以进一步管理生命周期 - ChartEditorModel model = + var model = new ChartEditorModel(workspacePath, chartMetadataIndex, chartPackData, chartData); + // 初始化脏状态服务,开始追踪未保存数据 + dirtyServer = new ChartEditorDirtyServer(model, initialHasUnsavedChanges); + // 初始化一些 Manager musicManager.Init(model); chartEditorNoteAudioManager.Init(model); @@ -84,7 +89,7 @@ public void StartBind(string workspacePath, var toolbarViewModel = new ToolbarViewModel(model).AddTo(Disposables); toolbarView.Bind(toolbarViewModel); - var menuButtonsViewModel = new MenuButtonsViewModel(model).AddTo(Disposables); + var menuButtonsViewModel = new MenuButtonsViewModel(model, dirtyServer).AddTo(Disposables); menuButtonsView.Bind(menuButtonsViewModel); var editorAttributeViewModel = new EditorAttributeViewModel(model).AddTo(Disposables); @@ -122,6 +127,10 @@ public void StartBind(string workspacePath, /// 退出制谱器时解除所有绑定,以释放内存 /// /// VM 通过 CatAsset 加载的资源也应该在此时由 VM 管理释放 - private void OnDestroy() => Disposables.Dispose(); + private void OnDestroy() + { + Disposables.Dispose(); + dirtyServer?.Dispose(); + } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs index 8c6e01f13..39895c85d 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs @@ -11,30 +11,23 @@ namespace CyanStars.Gameplay.ChartEditor.ViewModel { public class MenuButtonsViewModel : BaseViewModel { - public MenuButtonsViewModel(ChartEditorModel model) - : base(model) - { - } + private readonly ChartEditorDirtyServer DirtyServer; - public void ExitChartEditor() + public MenuButtonsViewModel(ChartEditorModel model, ChartEditorDirtyServer dirtyServer) + : base(model) { - GameRoot.ChangeProcedure(); + DirtyServer = dirtyServer; } - public void Undo() - { - base.CommandStack.Undo(); - } + public void ExitChartEditor() => GameRoot.ChangeProcedure(); - public void Redo() - { - base.CommandStack.Redo(); - } + public void Undo() => base.CommandStack.Undo(); + public void Redo() => base.CommandStack.Redo(); /// - /// 是否存在未保存数据(透传命令栈的状态) + /// 是否存在未保存数据 /// - public ReadOnlyReactiveProperty HasUnsavedChanges => CommandStack.HasUnsavedChanges; + public ReadOnlyReactiveProperty HasUnsavedChanges => DirtyServer.HasUnsavedChanges; public void SaveFileToDisk() { @@ -47,7 +40,7 @@ public void SaveFileToDisk() // 只有保存成功才标记为已保存,失败保持未保存状态便于重试 if (isSaveSuccess) - CommandStack.MarkSaved(); + DirtyServer.MarkSaved(); } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/NoteAttributeViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/NoteAttributeViewModel.cs index ddba5f7f9..78f174d26 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/NoteAttributeViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/NoteAttributeViewModel.cs @@ -5,6 +5,7 @@ using CyanStars.Gameplay.ChartEditor.Command; using CyanStars.Gameplay.ChartEditor.Model; using R3; +using UnityEngine; namespace CyanStars.Gameplay.ChartEditor.ViewModel { @@ -93,6 +94,9 @@ public void UpdateNoteJudgeBeat(string integerPart, string numerator, string den return; } + if (Model.SelectedNoteData.CurrentValue.JudgeBeat == newJudgeBeat) + return; + Beat oldJudgeBeat = Model.SelectedNoteData.CurrentValue.JudgeBeat; var note = Model.SelectedNoteData.CurrentValue; CommandStack.ExecuteCommand( @@ -139,6 +143,9 @@ public void UpdateNoteEndJudgeBeat(string integerPart, string numerator, string return; } + if (note.EndJudgeBeat == newEndBeat) + return; + Beat oldEndBeat = note.EndJudgeBeat; CommandStack.ExecuteCommand( () => @@ -168,6 +175,9 @@ public void UpdateNotePos(string pos) return; } + if (Mathf.Approximately(((IChartNoteNormalPos)Model.SelectedNoteData.CurrentValue).Pos, newPosFloat)) + return; + float oldPosFloat = ((IChartNoteNormalPos)Model.SelectedNoteData.CurrentValue).Pos; var note = (IChartNoteNormalPos)Model.SelectedNoteData.CurrentValue; CommandStack.ExecuteCommand( @@ -192,6 +202,9 @@ public void UpdateBreakNotePos(BreakNotePos newBreakPos) if (Model.SelectedNoteData.CurrentValue.Type != NoteType.Break) throw new Exception("SelectedNoteData is not break"); + if (((BreakChartNoteData)Model.SelectedNoteData.CurrentValue).BreakNotePos == newBreakPos) + return; + BreakNotePos oldBreakPos = ((BreakChartNoteData)Model.SelectedNoteData.CurrentValue).BreakNotePos; var note = (BreakChartNoteData)Model.SelectedNoteData.CurrentValue; CommandStack.ExecuteCommand( From 3f1acacc514a52e14b1c0ac479dc6f950227eef4 Mon Sep 17 00:00:00 2001 From: CL <62653664+Chen-Luan@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:29:46 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E5=91=BD=E4=BB=A4=E6=A0=88?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20tag=20=E4=B8=8E=E6=9C=AA=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 命令携带 affectsSavedData tag,纯视图/选中命令不参与脏判定 - 脏状态迁移到 CommandStack:基于保存边界条目的引用比较判定,历史上限 100 条 - 删除 ChartEditorDirtyServer,未保存状态改由命令栈维护 - 拖拽预览等旁路写点使用 MarkDirty 置脏 --- .../ChartEditor/ChartEditorDirtyServer.cs | 82 ---------- .../ChartEditorDirtyServer.cs.meta | 3 - .../ChartEditor/Command/CommandStack.cs | 151 ++++++++++++++++-- .../ChartEditor/Command/CommandStackExtend.cs | 12 +- .../ChartEditor/Management/MvvmBindManager.cs | 19 +-- .../ChartEditor/View/BasePopupView.cs | 3 +- .../ViewModel/BpmGroupViewModel.cs | 3 +- .../ViewModel/ChartPackDataCoverViewModel.cs | 9 ++ .../ViewModel/MenuButtonsViewModel.cs | 9 +- .../ViewModel/MusicVersionViewModel.cs | 4 +- 10 files changed, 175 insertions(+), 120 deletions(-) delete mode 100644 Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs delete mode 100644 Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs deleted file mode 100644 index 07ac905db..000000000 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs +++ /dev/null @@ -1,82 +0,0 @@ -#nullable enable - -using System; -using CyanStars.Gameplay.ChartEditor.Model; -using ObservableCollections; -using R3; - -namespace CyanStars.Gameplay.ChartEditor.Management -{ - /// - /// 制谱器"是否存在未保存数据"的服务 - /// - public class ChartEditorDirtyServer : IDisposable - { - private readonly CompositeDisposable Disposables = new CompositeDisposable(); - private readonly ReactiveProperty hasUnsavedChanges = new ReactiveProperty(false); - - /// - /// 是否存在未保存数据 - /// - public ReadOnlyReactiveProperty HasUnsavedChanges => hasUnsavedChanges; - - - /// - /// 绑定到制谱器 Model - /// - /// 制谱器 Model - /// 会话初始是否存在未保存数据(新建谱面为 true,加载已有谱面为 false) - public ChartEditorDirtyServer(ChartEditorModel model, bool initialHasUnsavedChanges) - { - hasUnsavedChanges.Value = initialHasUnsavedChanges; - - GetDataChangeSource(model) - .Subscribe(_ => hasUnsavedChanges.Value = true) - .AddTo(Disposables); - } - - /// - /// 标记为已保存(保存成功后调用) - /// - public void MarkSaved() => hasUnsavedChanges.Value = false; - - public void Dispose() => Disposables.Dispose(); - - - private static Observable GetDataChangeSource(ChartEditorModel model) - { - var cp = model.ChartPackData.CurrentValue; - var cd = model.ChartData.CurrentValue; - - return Observable.Merge( - cd.ReadyBeat.AsObservable().Skip(1).Select(_ => Unit.Default), - ToUnit(cd.SpeedGroupDatas), - ToUnit(cd.Notes), - ToUnit(cd.TrackDatas), - cp.DataVersion.AsObservable().Skip(1).Select(_ => Unit.Default), - cp.Title.AsObservable().Skip(1).Select(_ => Unit.Default), - cp.ChartPackInfo.AsObservable().Skip(1).Select(_ => Unit.Default), - cp.MusicPreviewStartBeat.AsObservable().Skip(1).Select(_ => Unit.Default), - cp.MusicPreviewEndBeat.AsObservable().Skip(1).Select(_ => Unit.Default), - cp.CoverFilePath.AsObservable().Skip(1).Select(_ => Unit.Default), - cp.CropStartPositionPercent.AsObservable().Skip(1).Select(_ => Unit.Default), - cp.CropHeightPercent.AsObservable().Skip(1).Select(_ => Unit.Default), - ToUnit(cp.MusicVersions), - ToUnit(cp.BpmGroup), - ToUnit(cp.ChartMetaDatas), - - // 列表子项修改 - model.BpmGroupDataChangedSubject.Select(_ => Unit.Default), - model.SelectedNoteDataChangedSubject.Select(_ => Unit.Default) - ); - } - - private static Observable ToUnit(ObservableList list) => - Observable.Merge( - list.ObserveAdd().Select(_ => Unit.Default), - list.ObserveRemove().Select(_ => Unit.Default), - list.ObserveReplace().Select(_ => Unit.Default), - list.ObserveReset().Select(_ => Unit.Default) - ); - } -} diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta deleted file mode 100644 index b157256df..000000000 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ChartEditorDirtyServer.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 826d8cdef01f571ed36a9629a66120cc -timeCreated: 1786377845 diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs index 88587bcb6..cb8105516 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStack.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; +using R3; using UnityEngine; namespace CyanStars.Gameplay.ChartEditor.Command @@ -11,19 +12,75 @@ namespace CyanStars.Gameplay.ChartEditor.Command /// 使用 List 管理命令实例,以提供撤销重做功能 public class CommandStack : MonoBehaviour { - private readonly List CommandHistory = new List(); + private class CommandEntry + { + public readonly ICommand Command; + public readonly bool AffectsSavedData; + + public CommandEntry(ICommand command, bool affectsSavedData) + { + Command = command; + AffectsSavedData = affectsSavedData; + } + } + + private readonly List CommandHistory = new List(); + + // 历史记录上限,超出后从最旧开始丢弃,防止内存溢出 + private const int MaxHistoryCount = 100; // 指向当前"最后一条已执行"的命令的索引 // -1 表示没有任何命令被执行(初始状态或全部撤销) private int currentCommandIndex = -1; + // 保存边界:最近一次保存时数据状态对应的数据命令条目。 + private CommandEntry? savedDataEntry = null; + + // 旁路修改(不经过命令栈的数据写入)置脏标记,MarkSaved 时清除 + private bool forcedDirty = false; + + private readonly ReactiveProperty hasUnsavedChanges = new ReactiveProperty(false); + + /// + /// 是否存在未保存数据(订阅时立即推送当前值) + /// + public ReadOnlyReactiveProperty HasUnsavedChanges => hasUnsavedChanges; + + /// + /// 是否正在回放命令(Execute/Undo/Redo,tracked 类型据此跳过命令记录,防止回放再生成命令) + /// + public bool IsReplaying { get; private set; } = false; + + /// + /// 每次进入制谱器会话时初始化 + /// + /// 会话初始是否存在未保存数据(新建谱面为 true,加载已有谱面为 false) + public void Init(bool initialHasUnsavedChanges) + { + CommandHistory.Clear(); + currentCommandIndex = -1; + savedDataEntry = null; + forcedDirty = initialHasUnsavedChanges; + UpdateUnsavedState(); + } + /// /// 执行新命令 /// - public void ExecuteCommand(ICommand command) + /// 要执行的命令 + /// 该命令是否修改持久化数据。纯视图/选中状态的命令传 false,不参与脏判定 + public void ExecuteCommand(ICommand command, bool affectsSavedData = true) { // TODO: 用事件驱动以替换当前的命令调用,以避免 View/MonoBehaviour 的内存泄漏 - command.Execute(); + IsReplaying = true; + try + { + command.Execute(); + } + finally + { + IsReplaying = false; + } // 如果当前索引不是在列表末尾,需要丢弃当前位置之后的所有旧历史 if (currentCommandIndex < CommandHistory.Count - 1) @@ -33,10 +90,19 @@ public void ExecuteCommand(ICommand command) CommandHistory.RemoveRange(removeStartIndex, countToRemove); } - CommandHistory.Add(command); + CommandHistory.Add(new CommandEntry(command, affectsSavedData)); currentCommandIndex++; - // TODO: 可选添加最大历史记录限制,防止内存溢出 + // 超出历史上限时丢弃最旧的命令。 + // 若保存边界条目被丢弃,数据不可能再与磁盘一致,引用比较会保持脏状态直到下次保存 + if (CommandHistory.Count > MaxHistoryCount) + { + int overflowCount = CommandHistory.Count - MaxHistoryCount; + CommandHistory.RemoveRange(0, overflowCount); + currentCommandIndex -= overflowCount; + } + + UpdateUnsavedState(); } /// @@ -51,8 +117,18 @@ public void Undo() return; } - CommandHistory[currentCommandIndex].Undo(); + IsReplaying = true; + try + { + CommandHistory[currentCommandIndex].Command.Undo(); + } + finally + { + IsReplaying = false; + } + currentCommandIndex--; + UpdateUnsavedState(); } /// @@ -68,16 +144,69 @@ public void Redo() } currentCommandIndex++; - CommandHistory[currentCommandIndex].Execute(); + IsReplaying = true; + try + { + CommandHistory[currentCommandIndex].Command.Execute(); + } + finally + { + IsReplaying = false; + } + + UpdateUnsavedState(); } + // /// + // /// 清空历史记录 + // /// + // public void Clear() + // { + // CommandHistory.Clear(); + // currentCommandIndex = -1; + // savedDataEntry = null; + // UpdateUnsavedState(); + // } + /// - /// 清空历史记录 + /// 标记当前数据为已保存(保存成功后调用) /// - public void Clear() + public void MarkSaved() { - CommandHistory.Clear(); - currentCommandIndex = -1; + forcedDirty = false; + savedDataEntry = GetCurrentDataEntry(); + UpdateUnsavedState(); + } + + /// + /// 强制标记为有未保存数据(不经过命令栈的旁路数据修改使用) + /// + public void MarkDirty() + { + forcedDirty = true; + UpdateUnsavedState(); + } + + /// + /// 当前数据状态对应的最近一条数据命令条目(从当前索引向下找),无则为 null + /// + private CommandEntry? GetCurrentDataEntry() + { + for (int i = currentCommandIndex; i >= 0; i--) + { + if (CommandHistory[i].AffectsSavedData) + return CommandHistory[i]; + } + + return null; + } + + /// + /// 根据当前数据命令条目与保存边界条目是否相同来重算未保存状态 + /// + private void UpdateUnsavedState() + { + hasUnsavedChanges.Value = forcedDirty || savedDataEntry != GetCurrentDataEntry(); } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStackExtend.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStackExtend.cs index 31c1e94ea..cd88f0343 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStackExtend.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Command/CommandStackExtend.cs @@ -9,10 +9,14 @@ namespace CyanStars.Gameplay.ChartEditor.Command /// public static class CommandStackExtend { - public static void ExecuteCommand(this CommandStack commandStack, Action? executeAction, Action? undoAction) - { - commandStack.ExecuteCommand(new DelegateCommand(executeAction, undoAction)); - } + public static void ExecuteCommand( + this CommandStack commandStack, + Action? executeAction, + Action? undoAction, + bool affectsSavedData = true + ) + => commandStack.ExecuteCommand(new DelegateCommand(executeAction, undoAction), affectsSavedData); + private class DelegateCommand : ICommand { diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs index de3242c0e..492f959e0 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using CyanStars.Chart; +using CyanStars.Framework; using CyanStars.Gameplay.ChartEditor.Model; using CyanStars.Gameplay.ChartEditor.View; using CyanStars.Gameplay.ChartEditor.ViewModel; @@ -14,8 +15,6 @@ public class MvvmBindManager : MonoBehaviour { private readonly CompositeDisposable Disposables = new CompositeDisposable(); - private ChartEditorDirtyServer dirtyServer = null!; - [SerializeField] private ToolbarView toolbarView = null!; @@ -73,12 +72,14 @@ public void StartBind(string workspacePath, ShortcutManager shortcutManager, ChartEditorPlayerPrefsManager playerPrefsManager) { + var commandStack = GameRoot.GetDataModule().CommandStack; + + // 初始化命令栈,开始追踪未保存数据 + commandStack.Init(initialHasUnsavedChanges); + var model = new ChartEditorModel(workspacePath, chartMetadataIndex, chartPackData, chartData); - // 初始化脏状态服务,开始追踪未保存数据 - dirtyServer = new ChartEditorDirtyServer(model, initialHasUnsavedChanges); - // 初始化一些 Manager musicManager.Init(model); chartEditorNoteAudioManager.Init(model); @@ -89,7 +90,7 @@ public void StartBind(string workspacePath, var toolbarViewModel = new ToolbarViewModel(model).AddTo(Disposables); toolbarView.Bind(toolbarViewModel); - var menuButtonsViewModel = new MenuButtonsViewModel(model, dirtyServer).AddTo(Disposables); + var menuButtonsViewModel = new MenuButtonsViewModel(model).AddTo(Disposables); menuButtonsView.Bind(menuButtonsViewModel); var editorAttributeViewModel = new EditorAttributeViewModel(model).AddTo(Disposables); @@ -127,10 +128,6 @@ public void StartBind(string workspacePath, /// 退出制谱器时解除所有绑定,以释放内存 /// /// VM 通过 CatAsset 加载的资源也应该在此时由 VM 管理释放 - private void OnDestroy() - { - Disposables.Dispose(); - dirtyServer?.Dispose(); - } + private void OnDestroy() => Disposables.Dispose(); } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/BasePopupView.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/BasePopupView.cs index 2312ee979..5c8806f24 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/BasePopupView.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/View/BasePopupView.cs @@ -80,7 +80,8 @@ public void SetCanvasVisibility(bool visible) commandStack.ExecuteCommand( () => CanvasVisibility.Value = visible, - () => CanvasVisibility.Value = !visible + () => CanvasVisibility.Value = !visible, + affectsSavedData: false ); } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/BpmGroupViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/BpmGroupViewModel.cs index 34f7cf66d..12036bba9 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/BpmGroupViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/BpmGroupViewModel.cs @@ -59,7 +59,8 @@ public void SelectBpmItem(BpmGroupItem? newItem) var oldItem = selectedBpmItem.Value; CommandStack.ExecuteCommand( () => selectedBpmItem.Value = newItem, - () => selectedBpmItem.Value = oldItem + () => selectedBpmItem.Value = oldItem, + affectsSavedData: false ); } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs index 39f950ab7..24d77175e 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs @@ -351,8 +351,14 @@ public void OnHandlerDragging(CoverCropHandlerType handlerType, Vector2 percentP float newCropHeightPercent = newCropHeightPixel / coverPixelSize.y; // 实时更新 Model 数据以实现实时预览,不生成命令 + bool changed = cropData.CropStartPositionPercent.Value != newCropStartPercent || + cropData.CropHeightPercent.Value != newCropHeightPercent; cropData.CropStartPositionPercent.Value = newCropStartPercent; cropData.CropHeightPercent.Value = newCropHeightPercent; + + // 拖拽预览绕过命令栈直接写 Model,手动标记脏状态 + if (changed) + CommandStack.MarkDirty(); } /// @@ -392,6 +398,9 @@ public void OnFrameDragging(Vector2 deltaRatio) { Vector2 targetPercentPos = new Vector2(targetPixelPos.x / imgW, targetPixelPos.y / imgH); cropData.CropStartPositionPercent.Value = targetPercentPos; + + // 拖拽预览绕过命令栈直接写 Model,手动标记脏状态 + CommandStack.MarkDirty(); } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs index 39895c85d..31ce5f9fa 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MenuButtonsViewModel.cs @@ -11,12 +11,9 @@ namespace CyanStars.Gameplay.ChartEditor.ViewModel { public class MenuButtonsViewModel : BaseViewModel { - private readonly ChartEditorDirtyServer DirtyServer; - - public MenuButtonsViewModel(ChartEditorModel model, ChartEditorDirtyServer dirtyServer) + public MenuButtonsViewModel(ChartEditorModel model) : base(model) { - DirtyServer = dirtyServer; } public void ExitChartEditor() => GameRoot.ChangeProcedure(); @@ -27,7 +24,7 @@ public MenuButtonsViewModel(ChartEditorModel model, ChartEditorDirtyServer dirty /// /// 是否存在未保存数据 /// - public ReadOnlyReactiveProperty HasUnsavedChanges => DirtyServer.HasUnsavedChanges; + public ReadOnlyReactiveProperty HasUnsavedChanges => CommandStack.HasUnsavedChanges; public void SaveFileToDisk() { @@ -40,7 +37,7 @@ public void SaveFileToDisk() // 只有保存成功才标记为已保存,失败保持未保存状态便于重试 if (isSaveSuccess) - DirtyServer.MarkSaved(); + CommandStack.MarkSaved(); } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs index c12ed10fa..410cd7fdc 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs @@ -96,9 +96,11 @@ public void SelectEditingMusicVersionData(MusicVersionDataEditorModel? musicVers return; var oldValue = selectedMusicVersionData.CurrentValue; + // 纯选中状态变化,不影响持久化数据,不参与脏判定 CommandStack.ExecuteCommand( () => selectedMusicVersionData.Value = musicVersionData, - () => selectedMusicVersionData.Value = oldValue + () => selectedMusicVersionData.Value = oldValue, + affectsSavedData: false ); } From f24387c1191edeb54a384d4f55d80da0726a59b8 Mon Sep 17 00:00:00 2001 From: CL <62653664+Chen-Luan@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:30:11 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=96=84=E4=BB=A3=E7=90=86=20TrackedReactive?= =?UTF-8?q?Property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 TrackedReactiveProperty:值变化自动生成撤销命令、等值去重、回放抑制 - Model 层 9 个持久化字段 tracked 化,CommandStack 贯穿各构造 - 9 处纯单属性写点改为直接赋值,删除手写撤销 lambda 与判等守卫 - 裁剪框浮点相等比较改为近似比较,避免换算舍入误差误判 --- .../ChartEditor/Management/MvvmBindManager.cs | 2 +- .../ChartEditor/Model/ChartDataEditorModel.cs | 7 ++-- .../ChartEditor/Model/ChartEditorModel.cs | 8 ++-- .../Model/ChartMetaDataEditorModel.cs | 7 ++-- .../Model/ChartPackDataEditorModel.cs | 23 ++++++----- .../Model/MusicVersionDataEditorModel.cs | 11 ++--- .../Model/TrackedReactiveProperty.cs | 40 +++++++++++++++++++ .../Model/TrackedReactiveProperty.cs.meta | 2 + .../ViewModel/ChartDataViewModel.cs | 21 ++-------- .../ViewModel/ChartPackDataCoverViewModel.cs | 17 +++++--- .../ViewModel/ChartPackDataViewModel.cs | 38 +++--------------- .../ViewModel/MusicVersionViewModel.cs | 33 +++++---------- 12 files changed, 103 insertions(+), 106 deletions(-) create mode 100644 Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs create mode 100644 Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs.meta diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs index 492f959e0..bcd640fb6 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Management/MvvmBindManager.cs @@ -78,7 +78,7 @@ public void StartBind(string workspacePath, commandStack.Init(initialHasUnsavedChanges); var model = - new ChartEditorModel(workspacePath, chartMetadataIndex, chartPackData, chartData); + new ChartEditorModel(workspacePath, chartMetadataIndex, chartPackData, chartData, commandStack); // 初始化一些 Manager musicManager.Init(model); diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartDataEditorModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartDataEditorModel.cs index 28766d0db..f999e3e88 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartDataEditorModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartDataEditorModel.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Contracts; using System.Linq; using CyanStars.Chart; +using CyanStars.Gameplay.ChartEditor.Command; using ObservableCollections; using R3; @@ -15,16 +16,16 @@ public class ChartDataEditorModel { private readonly ChartData ChartData; - public readonly ReactiveProperty ReadyBeat; + public readonly TrackedReactiveProperty ReadyBeat; public readonly ObservableList SpeedGroupDatas; public readonly ObservableList Notes; public readonly ObservableList TrackDatas; - public ChartDataEditorModel(ChartData chartData) + public ChartDataEditorModel(ChartData chartData, CommandStack commandStack) { ChartData = chartData; - ReadyBeat = new ReactiveProperty(chartData.ReadyBeat); + ReadyBeat = new TrackedReactiveProperty(commandStack, chartData.ReadyBeat); SpeedGroupDatas = new ObservableList(chartData.SpeedGroupDatas); Notes = new ObservableList(chartData.Notes); TrackDatas = new ObservableList(chartData.TrackDatas); diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartEditorModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartEditorModel.cs index 4c886475c..8b28cdbf8 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartEditorModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartEditorModel.cs @@ -2,6 +2,7 @@ using CatAsset.Runtime; using CyanStars.Chart; +using CyanStars.Gameplay.ChartEditor.Command; using R3; using UnityEngine; @@ -97,13 +98,14 @@ public class ChartEditorModel public ChartEditorModel(string workspacePath, int chartMetaDataIndex, ChartPackData chartPackData, - ChartData chartData) + ChartData chartData, + CommandStack commandStack) { WorkspacePath = workspacePath; ChartMetaDataIndex = chartMetaDataIndex; - ChartPackData = new ReactiveProperty(new ChartPackDataEditorModel(chartPackData)); - ChartData = new ReactiveProperty(new ChartDataEditorModel(chartData)); + ChartPackData = new ReactiveProperty(new ChartPackDataEditorModel(chartPackData, commandStack)); + ChartData = new ReactiveProperty(new ChartDataEditorModel(chartData, commandStack)); } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartMetaDataEditorModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartMetaDataEditorModel.cs index b841ccf3b..550ec7ff8 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartMetaDataEditorModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartMetaDataEditorModel.cs @@ -2,6 +2,7 @@ using System.Diagnostics.Contracts; using CyanStars.Chart; +using CyanStars.Gameplay.ChartEditor.Command; using R3; namespace CyanStars.Gameplay.ChartEditor.Model @@ -12,13 +13,13 @@ namespace CyanStars.Gameplay.ChartEditor.Model public class ChartMetaDataEditorModel { public readonly ReactiveProperty FilePath; - public readonly ReactiveProperty Difficulty; + public readonly TrackedReactiveProperty Difficulty; public readonly ReactiveProperty ChartHash; - public ChartMetaDataEditorModel(ChartMetaData chartMetaData) + public ChartMetaDataEditorModel(ChartMetaData chartMetaData, CommandStack commandStack) { FilePath = new ReactiveProperty(chartMetaData.FilePath); - Difficulty = new ReactiveProperty(chartMetaData.Difficulty); + Difficulty = new TrackedReactiveProperty(commandStack, chartMetaData.Difficulty); ChartHash = new ReactiveProperty(chartMetaData.ChartHash); } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartPackDataEditorModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartPackDataEditorModel.cs index 0b4c059a1..6786102b4 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartPackDataEditorModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/ChartPackDataEditorModel.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Contracts; using System.Linq; using CyanStars.Chart; +using CyanStars.Gameplay.ChartEditor.Command; using ObservableCollections; using R3; using UnityEngine; @@ -15,35 +16,35 @@ namespace CyanStars.Gameplay.ChartEditor.Model public class ChartPackDataEditorModel { public readonly ReactiveProperty DataVersion; - public readonly ReactiveProperty Title; - public readonly ReactiveProperty ChartPackInfo; + public readonly TrackedReactiveProperty Title; + public readonly TrackedReactiveProperty ChartPackInfo; public readonly ObservableList MusicVersions; public readonly ObservableList BpmGroup; - public readonly ReactiveProperty MusicPreviewStartBeat; - public readonly ReactiveProperty MusicPreviewEndBeat; + public readonly TrackedReactiveProperty MusicPreviewStartBeat; + public readonly TrackedReactiveProperty MusicPreviewEndBeat; public readonly ReactiveProperty CoverFilePath; public readonly ReactiveProperty CropStartPositionPercent; public readonly ReactiveProperty CropHeightPercent; public readonly ObservableList ChartMetaDatas; - public ChartPackDataEditorModel(ChartPackData chartPackData) + public ChartPackDataEditorModel(ChartPackData chartPackData, CommandStack commandStack) { DataVersion = new ReactiveProperty(chartPackData.DataVersion); - Title = new ReactiveProperty(chartPackData.Title); - ChartPackInfo = new ReactiveProperty(chartPackData.ChartPackInfo); + Title = new TrackedReactiveProperty(commandStack, chartPackData.Title); + ChartPackInfo = new TrackedReactiveProperty(commandStack, chartPackData.ChartPackInfo); MusicVersions = new ObservableList( chartPackData.MusicVersionDatas - .Select(static v => new MusicVersionDataEditorModel(v)) + .Select(v => new MusicVersionDataEditorModel(v, commandStack)) ); BpmGroup = new ObservableList(chartPackData.BpmGroup); - MusicPreviewStartBeat = new ReactiveProperty(chartPackData.MusicPreviewStartBeat); - MusicPreviewEndBeat = new ReactiveProperty(chartPackData.MusicPreviewEndBeat); + MusicPreviewStartBeat = new TrackedReactiveProperty(commandStack, chartPackData.MusicPreviewStartBeat); + MusicPreviewEndBeat = new TrackedReactiveProperty(commandStack, chartPackData.MusicPreviewEndBeat); CoverFilePath = new ReactiveProperty(chartPackData.CoverFilePath); CropStartPositionPercent = new ReactiveProperty(chartPackData.CropStartPositionPercent); CropHeightPercent = new ReactiveProperty(chartPackData.CropHeightPercent); ChartMetaDatas = new ObservableList( chartPackData.ChartMetaDatas - .Select(static d => new ChartMetaDataEditorModel(d)) + .Select(d => new ChartMetaDataEditorModel(d, commandStack)) ); } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/MusicVersionDataEditorModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/MusicVersionDataEditorModel.cs index 34cf496ae..4cb8ec25e 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/MusicVersionDataEditorModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/MusicVersionDataEditorModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using CyanStars.Chart; +using CyanStars.Gameplay.ChartEditor.Command; using ObservableCollections; using R3; @@ -15,17 +16,17 @@ public class MusicVersionDataEditorModel { private readonly MusicVersionData MusicVersionData; - public readonly ReactiveProperty VersionTitle; + public readonly TrackedReactiveProperty VersionTitle; public readonly ReactiveProperty AudioFilePath; - public readonly ReactiveProperty Offset; + public readonly TrackedReactiveProperty Offset; - public MusicVersionDataEditorModel(MusicVersionData musicVersionData) + public MusicVersionDataEditorModel(MusicVersionData musicVersionData, CommandStack commandStack) { MusicVersionData = musicVersionData; - VersionTitle = new ReactiveProperty(musicVersionData.VersionTitle); + VersionTitle = new TrackedReactiveProperty(commandStack, musicVersionData.VersionTitle); AudioFilePath = new ReactiveProperty(musicVersionData.AudioFilePath); - Offset = new ReactiveProperty(musicVersionData.Offset); + Offset = new TrackedReactiveProperty(commandStack, musicVersionData.Offset); } /// diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs new file mode 100644 index 000000000..bfeca5c9b --- /dev/null +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs @@ -0,0 +1,40 @@ +#nullable enable + +using System.Collections.Generic; +using CyanStars.Gameplay.ChartEditor.Command; +using R3; + +namespace CyanStars.Gameplay.ChartEditor.Model +{ + /// + /// 带命令记录的响应式属性:值变化时自动生成撤销命令并压入 CommandStack + /// + public class TrackedReactiveProperty : ReactiveProperty + { + private readonly CommandStack CommandStack; + + public TrackedReactiveProperty(CommandStack commandStack, T initialValue) + : base(initialValue) + { + CommandStack = commandStack; + } + + public override T Value + { + set + { + var oldValue = CurrentValue; + base.Value = value; + + // 基类构造期间字段尚未赋值,跳过记录;回放中或值未变化时也不生成命令 + if (CommandStack == null || CommandStack.IsReplaying || EqualityComparer.Default.Equals(oldValue, value)) + return; + + CommandStack.ExecuteCommand( + () => base.Value = value, + () => base.Value = oldValue + ); + } + } + } +} diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs.meta b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs.meta new file mode 100644 index 000000000..30324a1a4 --- /dev/null +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/Model/TrackedReactiveProperty.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 866f80d7bf13b8040b888b3beb46ade4 \ No newline at end of file diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartDataViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartDataViewModel.cs index 3c72f7361..4baeca38a 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartDataViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartDataViewModel.cs @@ -1,7 +1,6 @@ #nullable enable using CyanStars.Chart; -using CyanStars.Gameplay.ChartEditor.Command; using CyanStars.Gameplay.ChartEditor.Model; using R3; @@ -35,15 +34,8 @@ public ChartDataViewModel(ChartEditorModel model) public void SetChartDifficulty(ChartDifficulty? newDifficulty) { - var oldDifficulty = ChartDifficulty.CurrentValue; - - if (newDifficulty == oldDifficulty) - return; - - CommandStack.ExecuteCommand( - () => MetaData.Difficulty.Value = newDifficulty, - () => MetaData.Difficulty.Value = oldDifficulty - ); + // Difficulty 为 tracked 属性,等值赋值自动忽略,值变化则自动生成撤销命令 + MetaData.Difficulty.Value = newDifficulty; } public void SetReadyBeatCount(string newBeatCount) @@ -54,14 +46,7 @@ public void SetReadyBeatCount(string newBeatCount) return; } - var oldBeatIntCount = ChartData.ReadyBeat.Value; - if (newBeatCountInt == oldBeatIntCount) - return; - - CommandStack.ExecuteCommand( - () => ChartData.ReadyBeat.Value = newBeatCountInt, - () => ChartData.ReadyBeat.Value = oldBeatIntCount - ); + ChartData.ReadyBeat.Value = newBeatCountInt; } } } diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs index 24d77175e..9252c54c5 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataCoverViewModel.cs @@ -146,7 +146,10 @@ private void GetDefaultCoverCropData(Sprite sprite, out Vector2 startPosPercent, public void OpenCoverBrowser() { - GameRoot.File.OpenLoadFilePathBrowser(SetCoverFilePath, title: "打开曲绘", filters: new[] { GameRoot.File.SpriteFilter }); + GameRoot.File.OpenLoadFilePathBrowser(SetCoverFilePath, title: "打开曲绘", filters: new[] + { + GameRoot.File.SpriteFilter + }); } private void SetCoverFilePath(string newOriginFilePath) @@ -233,7 +236,9 @@ public void CommitCropData() Vector2? newCropStartPos = Model.ChartPackData.CurrentValue.CropStartPositionPercent.Value; float? newCropHeight = Model.ChartPackData.CurrentValue.CropHeightPercent.Value; - if (newCropStartPos == recordedCropStartPosPercent && newCropHeight == recordedCropHeightPercent) + if (Mathf.Approximately(newCropStartPos?.x ?? 0f, recordedCropStartPosPercent?.x ?? 0f) && + Mathf.Approximately(newCropStartPos?.y ?? 0f, recordedCropStartPosPercent?.y ?? 0f) && + Mathf.Approximately(newCropHeight ?? 0f, recordedCropHeightPercent ?? 0f)) return; CommandStack.ExecuteCommand( @@ -351,8 +356,9 @@ public void OnHandlerDragging(CoverCropHandlerType handlerType, Vector2 percentP float newCropHeightPercent = newCropHeightPixel / coverPixelSize.y; // 实时更新 Model 数据以实现实时预览,不生成命令 - bool changed = cropData.CropStartPositionPercent.Value != newCropStartPercent || - cropData.CropHeightPercent.Value != newCropHeightPercent; + bool changed = !Mathf.Approximately(cropData.CropStartPositionPercent.Value?.x ?? 0f, newCropStartPercent.x) || + !Mathf.Approximately(cropData.CropStartPositionPercent.Value?.y ?? 0f, newCropStartPercent.y) || + !Mathf.Approximately(cropData.CropHeightPercent.Value ?? 0f, newCropHeightPercent); cropData.CropStartPositionPercent.Value = newCropStartPercent; cropData.CropHeightPercent.Value = newCropHeightPercent; @@ -394,7 +400,8 @@ public void OnFrameDragging(Vector2 deltaRatio) targetPixelPos.x = Mathf.Clamp(targetPixelPos.x, 0f, maxX); targetPixelPos.y = Mathf.Clamp(targetPixelPos.y, 0f, maxY); - if (targetPixelPos != currentStartPixel) + if (!Mathf.Approximately(targetPixelPos.x, currentStartPixel.x) || + !Mathf.Approximately(targetPixelPos.y, currentStartPixel.y)) { Vector2 targetPercentPos = new Vector2(targetPixelPos.x / imgW, targetPixelPos.y / imgH); cropData.CropStartPositionPercent.Value = targetPercentPos; diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataViewModel.cs index dba586b19..d00d1c761 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/ChartPackDataViewModel.cs @@ -5,12 +5,10 @@ using System.IO; using CyanStars.Chart; using CyanStars.Framework; -using CyanStars.Gameplay.ChartEditor.Command; using CyanStars.Gameplay.ChartEditor.Management; using CyanStars.Gameplay.ChartEditor.Model; using CyanStars.Gameplay.ChartEditor.View; using CyanStars.Utils; -using ObservableCollections; using R3; using UnityEngine; @@ -76,14 +74,8 @@ public ChartPackDataViewModel(ChartEditorModel model) public void SetChartPackTitle(string newTitle) { - string oldTitle = Model.ChartPackData.CurrentValue.Title.Value; - if (newTitle == oldTitle) - return; - - CommandStack.ExecuteCommand( - () => Model.ChartPackData.CurrentValue.Title.Value = newTitle, - () => Model.ChartPackData.CurrentValue.Title.Value = oldTitle - ); + // Title 为 tracked 属性,等值赋值自动忽略,值变化则自动生成撤销命令 + Model.ChartPackData.CurrentValue.Title.Value = newTitle; } public void SetPreviewStartBeat(Beat newBeat) @@ -94,15 +86,7 @@ public void SetPreviewStartBeat(Beat newBeat) return; } - var oldBeat = Model.ChartPackData.CurrentValue.MusicPreviewStartBeat.Value; - - if (newBeat == oldBeat) - return; - - CommandStack.ExecuteCommand( - () => Model.ChartPackData.CurrentValue.MusicPreviewStartBeat.Value = newBeat, - () => Model.ChartPackData.CurrentValue.MusicPreviewStartBeat.Value = oldBeat - ); + Model.ChartPackData.CurrentValue.MusicPreviewStartBeat.Value = newBeat; } public void SetPreviewEndBeat(Beat newBeat) @@ -113,25 +97,13 @@ public void SetPreviewEndBeat(Beat newBeat) return; } - var oldBeat = Model.ChartPackData.CurrentValue.MusicPreviewEndBeat.Value; - - if (newBeat == oldBeat) - return; - - CommandStack.ExecuteCommand( - () => Model.ChartPackData.CurrentValue.MusicPreviewEndBeat.Value = newBeat, - () => Model.ChartPackData.CurrentValue.MusicPreviewEndBeat.Value = oldBeat - ); + Model.ChartPackData.CurrentValue.MusicPreviewEndBeat.Value = newBeat; } public void UpdateInfo(string newText) { // TODO: 实时更新字段 + 一段时间停止输入或失焦时压入 CommandStack - var oldText = Model.ChartPackData.CurrentValue.ChartPackInfo.CurrentValue; - CommandStack.ExecuteCommand( - () => Model.ChartPackData.CurrentValue.ChartPackInfo.Value = newText, - () => Model.ChartPackData.CurrentValue.ChartPackInfo.Value = oldText - ); + Model.ChartPackData.CurrentValue.ChartPackInfo.Value = newText; } public void ExportChartPack() diff --git a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs index 410cd7fdc..e2da2fbbc 100644 --- a/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs +++ b/Cyan-Stars/Assets/Scripts/Gameplay/ChartEditor/ViewModel/MusicVersionViewModel.cs @@ -106,7 +106,7 @@ public void SelectEditingMusicVersionData(MusicVersionDataEditorModel? musicVers public void AddMusicVersionItem() { - var newMusicVersionData = new MusicVersionDataEditorModel(new MusicVersionData("新音乐版本")); + var newMusicVersionData = new MusicVersionDataEditorModel(new MusicVersionData("新音乐版本"), CommandStack); CommandStack.ExecuteCommand( () => Model.ChartPackData.CurrentValue.MusicVersions.Add(newMusicVersionData), () => Model.ChartPackData.CurrentValue.MusicVersions.Remove(newMusicVersionData) @@ -174,13 +174,8 @@ public void SetTitle(string newTitle) if (SelectedMusicVersionData.CurrentValue == null) throw new InvalidOperationException("按设计,不允许在没有选中音乐版本数据的情况下设置标题。"); - var oldTitle = SelectedMusicVersionData.CurrentValue!.VersionTitle.Value; - if (oldTitle == newTitle) - return; - CommandStack.ExecuteCommand( - () => SelectedMusicVersionData.CurrentValue!.VersionTitle.Value = newTitle, - () => SelectedMusicVersionData.CurrentValue!.VersionTitle.Value = oldTitle - ); + // VersionTitle 为 tracked 属性,等值赋值自动忽略,值变化则自动生成撤销命令 + SelectedMusicVersionData.CurrentValue!.VersionTitle.Value = newTitle; } public void ImportAudioFile() @@ -263,10 +258,8 @@ public void MinusOffset() if (SelectedMusicVersionData.CurrentValue == null) throw new InvalidOperationException("按设计,不允许在没有选中音乐版本数据的情况下设置偏移量。"); - CommandStack.ExecuteCommand( - () => SelectedMusicVersionData.CurrentValue!.Offset.Value -= AddOffsetStep, - () => SelectedMusicVersionData.CurrentValue!.Offset.Value += AddOffsetStep - ); + // Offset 为 tracked 属性,值变化则自动生成撤销命令 + SelectedMusicVersionData.CurrentValue!.Offset.Value -= AddOffsetStep; } public void SetOffset(string text) @@ -280,13 +273,7 @@ public void SetOffset(string text) return; } - int oldValue = SelectedMusicVersionData.CurrentValue!.Offset.Value; - if (oldValue == newValue) - return; - CommandStack.ExecuteCommand( - () => SelectedMusicVersionData.CurrentValue!.Offset.Value = newValue, - () => SelectedMusicVersionData.CurrentValue!.Offset.Value = oldValue - ); + SelectedMusicVersionData.CurrentValue!.Offset.Value = newValue; } public void AddOffset() @@ -294,10 +281,7 @@ public void AddOffset() if (SelectedMusicVersionData.CurrentValue == null) throw new InvalidOperationException("按设计,不允许在没有选中音乐版本数据的情况下设置偏移量。"); - CommandStack.ExecuteCommand( - () => SelectedMusicVersionData.CurrentValue!.Offset.Value += AddOffsetStep, - () => SelectedMusicVersionData.CurrentValue!.Offset.Value -= AddOffsetStep - ); + SelectedMusicVersionData.CurrentValue!.Offset.Value += AddOffsetStep; } public void TestOffset() @@ -342,7 +326,8 @@ public void CloneItem() SelectedMusicVersionData.CurrentValue.VersionTitle.Value, SelectedMusicVersionData.CurrentValue.AudioFilePath.Value, SelectedMusicVersionData.CurrentValue.Offset.Value - ) + ), + CommandStack ); Model.ChartPackData.CurrentValue.MusicVersions.Add(deepClonedData);