feat(save): 增强开发存档跳转与严格校验

This commit is contained in:
2026-07-16 18:39:50 +08:00
parent 7b287a6248
commit 8deff54e48
6 changed files with 832 additions and 876 deletions
@@ -8,371 +8,371 @@ using AibisDream.SaveSystem;
using AibisDream.Utility;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
namespace AibisDream.EditorTools
{
/// <summary>
/// 将本机 testsavs 归档提升到 StreamingAssets/TestSaveFiles,便于提交与跨机器复用。
/// </summary>
/// <summary>从本机 testsavs 选择章节、去重排序并提升为可提交测试档。</summary>
public sealed class DevSavePromoteWindow : EditorWindow
{
private const string HuoshanSectionId = "Huoshan1";
private const string HuoshanSceneName = "Scene/HuoShanFixScene";
private const string HuoshanYarnProjectId = "FP_Huoshan1";
private static readonly (string NodeName, string Label, string FolderName)[] HuoshanCheckpoints =
{
("开头对话", "Stage1 开场", "01_Stage1"),
("Stage2就绪", "Stage2 初检", "02_Stage2"),
("Stage3就绪", "Stage3 滤波器", "03_Stage3"),
("Stage4就绪", "Stage4 表达深入", "04_Stage4"),
("Stage5就绪", "Stage5 中场对话", "05_Stage5"),
("Stage6就绪", "Stage6 LOG 释放", "06_Stage6"),
("Stage7就绪", "Stage7 最终检查", "07_Stage7"),
("结束对话", "Stage8 结束", "08_Stage8"),
};
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 readonly List<ArchiveGroup> groups = new();
private readonly List<Candidate> preview = new();
private int selectedGroup;
private string sectionId = string.Empty;
private string sectionTitle = string.Empty;
private string status = string.Empty;
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.minSize = new Vector2(720f, 520f);
window.Show();
}
private void OnEnable() => RefreshGroups();
private void OnGUI()
{
EditorGUILayout.LabelField("Promote testsavs → StreamingAssets/TestSaveFiles", EditorStyles.boldLabel);
EditorGUILayout.LabelField("测试存档提升与校验", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"本机 TestAutoSaveArchivetestsavs)筛选存档,复制到可提交目录。\n" +
$"目标: {ConstRef.TestSaveFilePath}",
"来源是本机 testsavs;目标是可提交、可随 Development Build 分发的 StreamingAssets/TestSaveFiles。",
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 (GUILayout.Button("刷新本机归档", GUILayout.Height(26f))) RefreshGroups();
if (GUILayout.Button("校验全部已提交测试档", GUILayout.Height(26f))) ValidateCommittedCatalog();
}
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)
if (groups.Count == 0)
{
EditorGUILayout.LabelField(
$"{item.FolderName} | node={item.NodeName} | yarn={item.YarnProjectId} | scene={item.SceneName}");
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 RefreshPreview()
private void DrawStatus()
{
preview = CollectCandidates();
status = $"Preview {preview.Count} entries from {ConstRef.TestAutoSaveArchivePath}";
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<Candidate>();
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 (string.IsNullOrWhiteSpace(sectionId))
if (preview.Count == 0 || string.IsNullOrWhiteSpace(sectionId))
{
status = "Section Id 不能为空。";
status = "没有可提升阶段,或 Section Id 为空。";
return;
}
var candidates = CollectCandidates();
if (candidates.Count == 0)
var validation = ValidateCandidates(preview);
if (validation.Count > 0)
{
status = "没有匹配的 testsavs 条目。先在 Editor 里打通流程(测试存档模式会写入 testsavs)。";
preview = candidates;
status = string.Join("\n", validation);
return;
}
if (IsHuoshanSection()
&& HuoshanCheckpoints.Any(checkpoint => candidates.All(c => c.NodeName != checkpoint.NodeName)))
{
var missing = HuoshanCheckpoints
.Where(checkpoint => candidates.All(c => c.NodeName != checkpoint.NodeName))
.Select(checkpoint => checkpoint.NodeName);
status = $"Huoshan1 缺少阶段存档:{string.Join(", ", missing)}。请完整跑通流程后再提升。";
preview = candidates;
return;
}
var sectionDir = Path.Combine(ConstRef.TestSaveFilePath, sectionId.Trim());
if ((clearSectionFirst || IsHuoshanSection()) && Directory.Exists(sectionDir))
Directory.Delete(sectionDir, recursive: true);
Directory.CreateDirectory(sectionDir);
EnsureCatalogSection(sectionId.Trim());
int copied = 0;
int index = 1;
foreach (var item in OrderCandidates(candidates))
{
var safeNode = SanitizeFolderName(string.IsNullOrEmpty(item.NodeName) ? "no_node" : item.NodeName);
var folderName = IsHuoshanSection()
? HuoshanCheckpoints.First(checkpoint => checkpoint.NodeName == item.NodeName).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 FilterHuoshanCheckpoints(list.OrderBy(c => c.SavedAt, StringComparer.Ordinal).ToList());
var latestPerNode = 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();
return FilterHuoshanCheckpoints(latestPerNode);
}
private bool IsHuoshanSection()
{
return string.Equals(sectionId?.Trim(), HuoshanSectionId, StringComparison.OrdinalIgnoreCase);
}
private List<Candidate> FilterHuoshanCheckpoints(List<Candidate> candidates)
{
if (!IsHuoshanSection())
return candidates;
var allowedNodes = new HashSet<string>(
HuoshanCheckpoints.Select(checkpoint => checkpoint.NodeName),
StringComparer.Ordinal);
return candidates.Where(candidate => allowedNodes.Contains(candidate.NodeName)).ToList();
}
private IEnumerable<Candidate> OrderCandidates(List<Candidate> candidates)
{
if (!IsHuoshanSection())
{
return candidates.OrderBy(c => c.SavedAt, StringComparer.Ordinal)
.ThenBy(c => c.NodeName, StringComparer.Ordinal);
}
return HuoshanCheckpoints.Select(checkpoint =>
candidates.First(candidate => candidate.NodeName == checkpoint.NodeName));
}
private static void EnsureCatalogSection(string sectionId)
{
var group = groups[selectedGroup];
var root = ConstRef.TestSaveFilePath;
Directory.CreateDirectory(root);
var catalogPath = Path.Combine(root, "catalog.json");
var targetSection = Path.Combine(root, sectionId);
if (Directory.Exists(targetSection)) Directory.Delete(targetSection, true);
Directory.CreateDirectory(targetSection);
CatalogDto catalog;
if (File.Exists(catalogPath))
var catalog = LoadCatalogForWrite();
catalog.sections.RemoveAll(section => string.Equals(section.id, sectionId, StringComparison.OrdinalIgnoreCase));
var sectionDto = new DevSaveCatalogSectionDto
{
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);
WriteCatalog(catalog);
AssetDatabase.Refresh();
ValidateCommittedCatalog();
}
private static List<string> ValidateCandidates(IEnumerable<Candidate> candidates)
{
var errors = new List<string>();
var nodes = new HashSet<string>(StringComparer.Ordinal);
foreach (var item in candidates)
{
if (!nodes.Add(item.NodeName)) errors.Add($"重复节点:{item.NodeName}");
try
{
catalog = JsonConvert.DeserializeObject<CatalogDto>(File.ReadAllText(catalogPath, Encoding.UTF8))
?? new CatalogDto();
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
catch (Exception ex)
{
catalog = new CatalogDto();
errors.Add($"{item.NodeName}: {ex.Message}");
}
}
else
{
catalog = new CatalogDto();
}
return errors;
}
catalog.sections ??= new List<CatalogSectionDto>();
var section = catalog.sections.FirstOrDefault(
s => string.Equals(s.id, sectionId, StringComparison.OrdinalIgnoreCase));
if (section == null)
private void ValidateCommittedCatalog()
{
var sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath);
var errors = new List<string>();
if (!string.IsNullOrWhiteSpace(DevSaveCatalog.LastError)) errors.Add(DevSaveCatalog.LastError);
foreach (var section in sections)
{
section = new CatalogSectionDto
var orders = new HashSet<int>();
var nodes = new HashSet<string>(StringComparer.Ordinal);
foreach (var entry in section.Entries)
{
id = sectionId,
title = sectionId
};
catalog.sections.Add(section);
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}");
}
}
if (string.Equals(sectionId, HuoshanSectionId, StringComparison.OrdinalIgnoreCase))
var dto = LoadCatalogForWrite();
foreach (var section in dto.sections)
{
section.title = "Huoshan 1";
section.expectedSceneName = HuoshanSceneName;
section.expectedYarnProjectId = HuoshanYarnProjectId;
section.allowedNodes = HuoshanCheckpoints.Select(checkpoint => checkpoint.NodeName).ToList();
section.entries = HuoshanCheckpoints.Select(checkpoint => new CatalogEntryDto
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
{
label = checkpoint.Label,
path = $"{HuoshanSectionId}/{checkpoint.FolderName}",
anchorNode = checkpoint.NodeName
}).ToList();
var nodeNames = new HashSet<string>(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<TalkSceneSO>(AssetDatabase.GUIDToAssetPath(guid));
if (asset != null && asset.name == assetName) return asset;
}
return null;
}
private static DevSaveCatalogDto LoadCatalogForWrite()
{
var path = Path.Combine(ConstRef.TestSaveFilePath, "catalog.json");
if (!File.Exists(path)) return new DevSaveCatalogDto();
try
{
return JsonConvert.DeserializeObject<DevSaveCatalogDto>(File.ReadAllText(path, Encoding.UTF8))
?? new DevSaveCatalogDto();
}
catch
{
return new DevSaveCatalogDto();
}
}
private static void WriteCatalog(DevSaveCatalogDto catalog)
{
Directory.CreateDirectory(ConstRef.TestSaveFilePath);
File.WriteAllText(
catalogPath,
Path.Combine(ConstRef.TestSaveFilePath, "catalog.json"),
JsonConvert.SerializeObject(catalog, Formatting.Indented),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
new UTF8Encoding(false));
}
private static void CopyIfExists(string src, string dest)
private static string SanitizeIdentifier(string value)
{
if (string.IsNullOrEmpty(src) || !File.Exists(src))
return;
File.Copy(src, dest, overwrite: true);
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 static string SanitizeFolderName(string name)
private sealed class ArchiveGroup
{
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;
public readonly List<Candidate> Items;
public string SceneName => Items[0].SceneName;
public string SceneSoName => Items[0].SceneSoName;
public string YarnProjectId => Items[0].YarnProjectId;
public ArchiveGroup(List<Candidate> items) => Items = items;
}
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 SceneSoName;
public readonly string YarnProjectId;
public readonly string NodeName;
public readonly string SavedAt;
public string Label;
public Candidate(
string folderName,
string directoryPath,
string snapshotPath,
string metaPath,
string nodeName,
string yarnProjectId,
string sceneName,
string savedAt)
public Candidate(string snapshotPath, string metaPath, string sceneName, string sceneSoName,
string yarnProjectId, string nodeName, string savedAt)
{
FolderName = folderName;
DirectoryPath = directoryPath;
SnapshotPath = snapshotPath;
MetaPath = metaPath;
NodeName = nodeName;
YarnProjectId = yarnProjectId;
SceneName = sceneName;
SavedAt = savedAt;
SceneSoName = sceneSoName;
YarnProjectId = yarnProjectId;
NodeName = nodeName;
SavedAt = savedAt ?? string.Empty;
Label = nodeName;
}
}
[Serializable]
private class CatalogDto
{
public List<CatalogSectionDto> sections = new();
}
[Serializable]
private class CatalogSectionDto
{
public string id;
public string title;
public List<CatalogEntryDto> entries;
public string expectedSceneName;
public string expectedYarnProjectId;
public List<string> allowedNodes;
}
[Serializable]
private class CatalogEntryDto
{
public string label;
public string path;
public string anchorNode;
}
}
}
#endif