feat: 存档系统增加退出节点保存时机

This commit is contained in:
2026-07-26 19:59:56 +08:00
parent 4caf70b554
commit 82c15e8f48
18 changed files with 1195 additions and 159 deletions
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AibisDream.SaveSystem
{
@@ -16,6 +17,7 @@ namespace AibisDream.SaveSystem
{
public TestSaveCoverageRowKind Kind { get; internal set; }
public string NodeName { get; internal set; }
public string ResumeMode { get; internal set; }
public TestSaveEntry Entry { get; internal set; }
public long SortOrder { get; internal set; }
}
@@ -49,7 +51,7 @@ namespace AibisDream.SaveSystem
var chapter = chapters[chapterIndex];
if (chapter == null) continue;
var expectedNodes = GetExpectedNodes(chapter);
var expectedCheckpoints = GetExpectedCheckpoints(chapter);
var chapterEntries = entries
.Where(item => item.Meta != null
&& string.Equals(item.Meta.sceneSoName, chapter.name, StringComparison.Ordinal)
@@ -60,33 +62,37 @@ namespace AibisDream.SaveSystem
.ToArray();
foreach (var entry in chapterEntries) consumed.Add(entry);
var validByNode = chapterEntries
var validByCheckpoint = chapterEntries
.Where(item => item.IsValid)
.GroupBy(item => item.NodeName, StringComparer.Ordinal)
.GroupBy(
item => BuildCheckpointKey(item.NodeName, item.ResumeMode),
StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group.OrderBy(item => item.Meta.firstSeenOrder).First(),
StringComparer.Ordinal);
var rows = new List<TestSaveCoverageRow>();
foreach (var entry in validByNode.Values.OrderBy(item => item.Meta.firstSeenOrder))
foreach (var entry in validByCheckpoint.Values.OrderBy(item => item.Meta.firstSeenOrder))
{
rows.Add(new TestSaveCoverageRow
{
Kind = TestSaveCoverageRowKind.Recorded,
NodeName = entry.NodeName,
ResumeMode = entry.ResumeMode,
Entry = entry,
SortOrder = entry.Meta.firstSeenOrder
});
}
foreach (var nodeName in expectedNodes
.Where(node => !validByNode.ContainsKey(node))
.OrderBy(node => node, StringComparer.OrdinalIgnoreCase))
foreach (var expectation in expectedCheckpoints.Values
.Where(item => !validByCheckpoint.ContainsKey(item.Key))
.OrderBy(item => item.NodeName, StringComparer.OrdinalIgnoreCase))
{
rows.Add(new TestSaveCoverageRow
{
Kind = TestSaveCoverageRowKind.Missing,
NodeName = nodeName,
NodeName = expectation.NodeName,
ResumeMode = expectation.ResumeMode,
SortOrder = long.MaxValue
});
}
@@ -98,6 +104,7 @@ namespace AibisDream.SaveSystem
{
Kind = TestSaveCoverageRowKind.Invalid,
NodeName = entry.NodeName ?? "(未知节点)",
ResumeMode = entry.ResumeMode,
Entry = entry,
SortOrder = entry.Meta?.firstSeenOrder ?? long.MaxValue
});
@@ -108,8 +115,8 @@ namespace AibisDream.SaveSystem
ChapterId = chapter.name,
Title = string.IsNullOrWhiteSpace(chapter.title) ? chapter.name : chapter.title,
ChapterOrder = chapterIndex,
RecordedCount = validByNode.Keys.Count(expectedNodes.Contains),
ExpectedCount = expectedNodes.Count,
RecordedCount = validByCheckpoint.Keys.Count(expectedCheckpoints.ContainsKey),
ExpectedCount = expectedCheckpoints.Count,
Rows = rows
});
}
@@ -131,6 +138,7 @@ namespace AibisDream.SaveSystem
{
Kind = TestSaveCoverageRowKind.Invalid,
NodeName = item.NodeName ?? "(未知节点)",
ResumeMode = item.ResumeMode,
Entry = item,
SortOrder = item.Meta?.firstSeenOrder ?? long.MaxValue
})
@@ -143,7 +151,14 @@ namespace AibisDream.SaveSystem
public static HashSet<string> GetExpectedNodes(TalkSceneSO chapter)
{
var result = new HashSet<string>(StringComparer.Ordinal);
return GetExpectedCheckpoints(chapter).Values
.Select(item => item.NodeName)
.ToHashSet(StringComparer.Ordinal);
}
private static Dictionary<string, ExpectedCheckpoint> GetExpectedCheckpoints(TalkSceneSO chapter)
{
var result = new Dictionary<string, ExpectedCheckpoint>(StringComparer.Ordinal);
var project = chapter?.yarnProject;
if (project?.Program?.Nodes == null) return result;
@@ -157,14 +172,65 @@ namespace AibisDream.SaveSystem
var tags = string.IsNullOrWhiteSpace(tagsValue)
? Array.Empty<string>()
: tagsValue.Split(Array.Empty<char>(), StringSplitOptions.RemoveEmptyEntries);
if (SavePointEvaluator.EvaluateNodeTagsForAutoSaveSilently(node.Name, tags, out _))
var policy = SavePointEvaluator.ResolveNodePolicy(
node.Name,
tags,
out var reason,
logWarnings: false);
if (reason == SavePointRejectReason.InvalidTagConfiguration)
{
result.Add(node.Name);
Debug.LogError(
$"[TestSaveCoverage] 节点 {node.Name} 的保存标签配置无效: " +
$"[{string.Join(", ", tags)}]");
}
else if (tags.Any(tag =>
string.Equals(tag, "interaction", StringComparison.OrdinalIgnoreCase))
&& tags.Any(tag =>
string.Equals(tag, "save_on_exit", StringComparison.OrdinalIgnoreCase)))
{
Debug.LogWarning(
$"[TestSaveCoverage] 节点 {node.Name} 的 interaction 已包含 save_on_exit 语义," +
"请移除重复标记。");
}
if (policy.Timing == NodeSaveTiming.None)
{
continue;
}
var resumeMode = policy.ResumeMode.ToString();
var key = BuildCheckpointKey(node.Name, resumeMode);
result[key] = new ExpectedCheckpoint(key, node.Name, resumeMode);
}
return result;
}
private static string BuildCheckpointKey(string nodeName, string resumeMode)
{
return $"{nodeName ?? string.Empty}\n{NormalizeResumeMode(resumeMode)}";
}
private static string NormalizeResumeMode(string resumeMode)
{
return string.IsNullOrEmpty(resumeMode)
? nameof(SaveResumeMode.RestartNode)
: resumeMode;
}
private readonly struct ExpectedCheckpoint
{
public readonly string Key;
public readonly string NodeName;
public readonly string ResumeMode;
public ExpectedCheckpoint(string key, string nodeName, string resumeMode)
{
Key = key;
NodeName = nodeName;
ResumeMode = resumeMode;
}
}
}
}
#endif