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