Files
aibis-dream/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs
T

292 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 UnityEditor;
using UnityEngine;
namespace AibisDream.EditorTools
{
/// <summary>
/// 将本机 testsavs 归档提升到 StreamingAssets/TestSaveFiles,便于提交与跨机器复用。
/// </summary>
public sealed class DevSavePromoteWindow : EditorWindow
{
private string sectionId = "Huoshan1";
private string yarnProjectFilter = "FP_Huoshan1";
private string sceneFilter = "Scene/HuoShanFixScene";
private bool matchYarn = true;
private bool matchScene = true;
private bool keepLatestPerNode = true;
private bool clearSectionFirst;
private Vector2 scroll;
private string status = "";
private List<Candidate> preview = new();
[MenuItem("Tools/Aibis/Dev Save Promote")]
public static void Open()
{
var window = GetWindow<DevSavePromoteWindow>("Dev Save Promote");
window.minSize = new Vector2(520, 420);
window.Show();
}
private void OnGUI()
{
EditorGUILayout.LabelField("Promote testsavs → StreamingAssets/TestSaveFiles", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"从本机 TestAutoSaveArchivetestsavs)筛选存档,复制到可提交目录。\n" +
$"目标: {ConstRef.TestSaveFilePath}",
MessageType.Info);
sectionId = EditorGUILayout.TextField("Section Id", sectionId);
matchYarn = EditorGUILayout.Toggle("Filter by YarnProject", matchYarn);
using (new EditorGUI.DisabledScope(!matchYarn))
yarnProjectFilter = EditorGUILayout.TextField("YarnProject Id", yarnProjectFilter);
matchScene = EditorGUILayout.Toggle("Filter by Scene", matchScene);
using (new EditorGUI.DisabledScope(!matchScene))
sceneFilter = EditorGUILayout.TextField("Scene Name", sceneFilter);
keepLatestPerNode = EditorGUILayout.Toggle("Keep latest per nodeName", keepLatestPerNode);
clearSectionFirst = EditorGUILayout.Toggle("Clear section folder first", clearSectionFirst);
EditorGUILayout.Space(8);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Refresh Preview", GUILayout.Height(28)))
RefreshPreview();
if (GUILayout.Button("Promote Selected Filters", GUILayout.Height(28)))
Promote();
}
if (!string.IsNullOrEmpty(status))
EditorGUILayout.HelpBox(status, MessageType.None);
EditorGUILayout.Space(4);
EditorGUILayout.LabelField($"Matches: {preview.Count}", EditorStyles.miniBoldLabel);
scroll = EditorGUILayout.BeginScrollView(scroll);
foreach (var item in preview)
{
EditorGUILayout.LabelField(
$"{item.FolderName} | node={item.NodeName} | yarn={item.YarnProjectId} | scene={item.SceneName}");
}
EditorGUILayout.EndScrollView();
}
private void RefreshPreview()
{
preview = CollectCandidates();
status = $"Preview {preview.Count} entries from {ConstRef.TestAutoSaveArchivePath}";
}
private void Promote()
{
if (string.IsNullOrWhiteSpace(sectionId))
{
status = "Section Id 不能为空。";
return;
}
var candidates = CollectCandidates();
if (candidates.Count == 0)
{
status = "没有匹配的 testsavs 条目。先在 Editor 里打通流程(测试存档模式会写入 testsavs)。";
preview = candidates;
return;
}
var sectionDir = Path.Combine(ConstRef.TestSaveFilePath, sectionId.Trim());
if (clearSectionFirst && Directory.Exists(sectionDir))
Directory.Delete(sectionDir, recursive: true);
Directory.CreateDirectory(sectionDir);
EnsureCatalogSection(sectionId.Trim());
int copied = 0;
int index = 1;
foreach (var item in candidates.OrderBy(c => c.SavedAt, StringComparer.Ordinal)
.ThenBy(c => c.NodeName, StringComparer.Ordinal))
{
var safeNode = SanitizeFolderName(string.IsNullOrEmpty(item.NodeName) ? "no_node" : item.NodeName);
var folderName = $"{index:00}_{safeNode}";
var destDir = Path.Combine(sectionDir, folderName);
Directory.CreateDirectory(destDir);
CopyIfExists(item.SnapshotPath, Path.Combine(destDir, $"{ConstRef.SaveSnapshotFileName}.json"));
CopyIfExists(item.MetaPath, Path.Combine(destDir, $"{ConstRef.SaveMetaFileName}.json"));
copied++;
index++;
}
preview = candidates;
AssetDatabase.Refresh();
status = $"已复制 {copied} 份到 TestSaveFiles/{sectionId.Trim()}";
}
private List<Candidate> CollectCandidates()
{
var list = new List<Candidate>();
if (!Directory.Exists(ConstRef.TestAutoSaveArchivePath))
return list;
foreach (var dir in Directory.GetDirectories(ConstRef.TestAutoSaveArchivePath))
{
var snapshotPath = Path.Combine(dir, $"{ConstRef.SaveSnapshotFileName}.json");
if (!File.Exists(snapshotPath))
continue;
var metaPath = Path.Combine(dir, $"{ConstRef.SaveMetaFileName}.json");
SlotMeta meta = null;
if (File.Exists(metaPath))
{
try
{
meta = JsonConvert.DeserializeObject<SlotMeta>(File.ReadAllText(metaPath, Encoding.UTF8));
}
catch
{
// ignore broken meta
}
}
var yarn = meta?.yarnProjectId ?? string.Empty;
var scene = meta?.sceneName ?? string.Empty;
var node = meta?.nodeName ?? string.Empty;
var savedAt = meta?.savedAt ?? string.Empty;
bool yarnOk = !matchYarn || string.Equals(yarn, yarnProjectFilter, StringComparison.OrdinalIgnoreCase);
bool sceneOk = !matchScene || string.Equals(scene, sceneFilter, StringComparison.OrdinalIgnoreCase);
if (!yarnOk || !sceneOk)
continue;
list.Add(new Candidate(
Path.GetFileName(dir),
dir,
snapshotPath,
File.Exists(metaPath) ? metaPath : null,
node,
yarn,
scene,
savedAt));
}
if (!keepLatestPerNode)
return list.OrderBy(c => c.SavedAt, StringComparer.Ordinal).ToList();
return list
.GroupBy(c => string.IsNullOrEmpty(c.NodeName) ? c.FolderName : c.NodeName, StringComparer.Ordinal)
.Select(g => g.OrderByDescending(c => c.SavedAt, StringComparer.Ordinal).First())
.OrderBy(c => c.SavedAt, StringComparer.Ordinal)
.ToList();
}
private static void EnsureCatalogSection(string sectionId)
{
var root = ConstRef.TestSaveFilePath;
Directory.CreateDirectory(root);
var catalogPath = Path.Combine(root, "catalog.json");
CatalogDto catalog;
if (File.Exists(catalogPath))
{
try
{
catalog = JsonConvert.DeserializeObject<CatalogDto>(File.ReadAllText(catalogPath, Encoding.UTF8))
?? new CatalogDto();
}
catch
{
catalog = new CatalogDto();
}
}
else
{
catalog = new CatalogDto();
}
catalog.sections ??= new List<CatalogSectionDto>();
if (catalog.sections.All(s => !string.Equals(s.id, sectionId, StringComparison.OrdinalIgnoreCase)))
{
catalog.sections.Add(new CatalogSectionDto
{
id = sectionId,
title = sectionId
});
File.WriteAllText(
catalogPath,
JsonConvert.SerializeObject(catalog, Formatting.Indented),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
}
private static void CopyIfExists(string src, string dest)
{
if (string.IsNullOrEmpty(src) || !File.Exists(src))
return;
File.Copy(src, dest, overwrite: true);
}
private static string SanitizeFolderName(string name)
{
foreach (var c in Path.GetInvalidFileNameChars())
name = name.Replace(c, '_');
name = name.Replace(' ', '_');
if (name.Length > 48)
name = name.Substring(0, 48);
return string.IsNullOrWhiteSpace(name) ? "no_node" : name;
}
private sealed class Candidate
{
public readonly string FolderName;
public readonly string DirectoryPath;
public readonly string SnapshotPath;
public readonly string MetaPath;
public readonly string NodeName;
public readonly string YarnProjectId;
public readonly string SceneName;
public readonly string SavedAt;
public Candidate(
string folderName,
string directoryPath,
string snapshotPath,
string metaPath,
string nodeName,
string yarnProjectId,
string sceneName,
string savedAt)
{
FolderName = folderName;
DirectoryPath = directoryPath;
SnapshotPath = snapshotPath;
MetaPath = metaPath;
NodeName = nodeName;
YarnProjectId = yarnProjectId;
SceneName = sceneName;
SavedAt = savedAt;
}
}
[Serializable]
private class CatalogDto
{
public List<CatalogSectionDto> sections = new();
}
[Serializable]
private class CatalogSectionDto
{
public string id;
public string title;
}
}
}
#endif