Merge branch 'develop' into 7.19春优化

This commit is contained in:
2026-07-20 03:50:37 +08:00
231 changed files with 11718 additions and 11089 deletions
@@ -396,8 +396,10 @@ namespace AibisDream.FixSystem
if (!useYarn)
{
DialogController.Instance.StartDialogNode(
LocalizationKit.GetL10NParamKey(punchTapeSlot.punchTape.ClueName));
if (!PunchTapeManager.Instance.TryGetDefinition(punchTapeSlot.punchTape, out var definition))
yield break;
DialogController.Instance.StartDialogNode(definition.DialogNode);
}
}
@@ -422,6 +424,12 @@ namespace AibisDream.FixSystem
_activePunchTape = punchTape;
if (!PunchTapeManager.Instance.TryGetDefinition(_activePunchTape, out var definition))
{
StartStandbyOverlay();
yield break;
}
if (isProcessing)
{
Debug.LogWarning("Already processing a memory. Please wait until the process is finished.");
@@ -431,7 +439,7 @@ namespace AibisDream.FixSystem
StopStandbyOverlay();
ShowSearchOverlay();
var searchMemoryKey = string.Format(MemorySpritePath, _activePunchTape.MemoryIndex);
var searchMemoryKey = string.Format(MemorySpritePath, definition.MemoryResourceKey);
var searchMemoryHandle = ResourceSystem.LoadAsync<Sprite>(searchMemoryKey);
yield return searchMemoryHandle;
Sprite memorySprite = searchMemoryHandle.Status == AsyncOperationStatus.Succeeded
@@ -145,9 +145,9 @@ namespace AibisDream.FixSystem
private void PlayMemory(PunchTape tape)
{
if (tape != null)
if (tape != null && PunchTapeManager.Instance.TryGetDefinition(tape, out var definition))
{
Debug.Log("播放记忆:" + tape.MemoryIndex);
Debug.Log("播放记忆:" + definition.MemoryResourceKey);
if (memoryManager != null)
memoryManager.StartCoroutine(memoryManager.SearchMemory(tape));
}
@@ -0,0 +1,51 @@
using System;
/// <summary>
/// 打孔带运行时与存档数据。流程、资源和显示信息统一由 DefinitionId 在 PunchTapeCatalog 中解析。
/// </summary>
namespace AibisDream.FixSystem
{
[Serializable]
public class PunchTape : IEquatable<PunchTape>
{
public string DefinitionId;
public bool used;
public PunchTape()
{
}
public PunchTape(string definitionId)
{
DefinitionId = definitionId;
}
/// <summary>用于收藏列表去重与 UI 行映射(与 Equals 一致)。</summary>
public string GetRowKey()
{
return DefinitionId ?? string.Empty;
}
public bool Equals(PunchTape other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return string.Equals(DefinitionId, other.DefinitionId, StringComparison.Ordinal);
}
public override bool Equals(object obj) => Equals(obj as PunchTape);
public override int GetHashCode()
{
return DefinitionId != null ? StringComparer.Ordinal.GetHashCode(DefinitionId) : 0;
}
public static bool operator ==(PunchTape left, PunchTape right)
{
if (left is null) return right is null;
return left.Equals(right);
}
public static bool operator !=(PunchTape left, PunchTape right) => !(left == right);
}
}
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace AibisDream.FixSystem
{
[Serializable]
public sealed class PunchTapeDefinition
{
[SerializeField] private string id;
[SerializeField] private string displayNameParam;
[SerializeField] private string dialogNode;
[SerializeField] private string memoryResourceKey;
[SerializeField] private string visibilityCode;
[SerializeField] private string contentId;
[SerializeField] private string timecode;
public string Id => id;
public string DisplayNameParam => displayNameParam;
public string DialogNode => dialogNode;
public string MemoryResourceKey => memoryResourceKey;
public string VisibilityCode => visibilityCode;
public string ContentId => contentId;
public string Timecode => timecode;
public bool TryValidate(out string error)
{
if (string.IsNullOrWhiteSpace(id))
{
error = "ID 不能为空。";
return false;
}
if (string.IsNullOrWhiteSpace(displayNameParam) ||
!displayNameParam.StartsWith("l10n.", StringComparison.Ordinal))
{
error = $"'{id}' 的名称必须配置为 l10n.* 参数键。";
return false;
}
if (string.IsNullOrWhiteSpace(dialogNode) || string.IsNullOrWhiteSpace(memoryResourceKey) ||
string.IsNullOrWhiteSpace(visibilityCode) || string.IsNullOrWhiteSpace(contentId) ||
string.IsNullOrWhiteSpace(timecode))
{
error = $"'{id}' 存在未填写的流程或显示字段。";
return false;
}
error = null;
return true;
}
#if UNITY_EDITOR
public void ConfigureForEditor(string definitionId, string localizedNameParam, string yarnDialogNode,
string resourceKey, string visibility, string cid, string timestamp)
{
id = definitionId;
displayNameParam = localizedNameParam;
dialogNode = yarnDialogNode;
memoryResourceKey = resourceKey;
visibilityCode = visibility;
contentId = cid;
timecode = timestamp;
}
#endif
}
[CreateAssetMenu(fileName = "PunchTapeCatalog", menuName = "AibisDream/Memory/Punch Tape Catalog")]
public sealed class PunchTapeCatalog : ScriptableObject
{
[SerializeField] private List<PunchTapeDefinition> definitions = new();
private Dictionary<string, PunchTapeDefinition> definitionById;
public IReadOnlyList<PunchTapeDefinition> Definitions => definitions;
public bool TryGetDefinition(string definitionId, out PunchTapeDefinition definition)
{
EnsureLookup();
if (string.IsNullOrWhiteSpace(definitionId))
{
definition = null;
return false;
}
return definitionById.TryGetValue(definitionId, out definition);
}
public List<string> GetValidationErrors()
{
var errors = new List<string>();
var ids = new HashSet<string>(StringComparer.Ordinal);
if (definitions == null || definitions.Count == 0)
{
errors.Add("Catalog 中没有打孔带定义。");
return errors;
}
for (var i = 0; i < definitions.Count; i++)
{
var definition = definitions[i];
if (definition == null)
{
errors.Add($"definitions[{i}] 为空。");
continue;
}
if (!definition.TryValidate(out var error))
errors.Add(error);
if (!string.IsNullOrWhiteSpace(definition.Id) && !ids.Add(definition.Id))
errors.Add($"存在重复 ID'{definition.Id}'。");
}
return errors;
}
private void EnsureLookup()
{
if (definitionById != null)
return;
definitionById = new Dictionary<string, PunchTapeDefinition>(StringComparer.Ordinal);
if (definitions == null)
return;
foreach (var definition in definitions)
{
if (definition == null || string.IsNullOrWhiteSpace(definition.Id) ||
definitionById.ContainsKey(definition.Id))
continue;
definitionById.Add(definition.Id, definition);
}
}
private void OnEnable()
{
definitionById = null;
}
#if UNITY_EDITOR
private void OnValidate()
{
definitionById = null;
}
public void SetDefinitionsForEditor(IEnumerable<PunchTapeDefinition> newDefinitions)
{
definitions = newDefinitions == null
? new List<PunchTapeDefinition>()
: new List<PunchTapeDefinition>(newDefinitions);
definitionById = null;
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 473f2b987f7c6ab40875d33a8ffe67a6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -179,7 +179,7 @@ namespace AibisDream.FixSystem
rt.localScale = Vector3.one;
}
private static void BindRow(GameObject rowGo, PunchTape tape)
private void BindRow(GameObject rowGo, PunchTape tape)
{
var interaction = rowGo.GetComponent<PunchTapeInteraction>();
if (interaction != null)
@@ -189,8 +189,8 @@ namespace AibisDream.FixSystem
}
var item = rowGo.GetComponent<PunchTapeItem>();
if (item != null)
item.Setup(tape);
if (item != null && PunchTapeManager.TryGetDefinition(tape, out var definition))
item.Setup(tape, definition);
}
/// <summary>
@@ -0,0 +1,96 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Localization.Components;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using TMPro;
using AibisDream.Framework;
/// <summary>
/// 收藏夹 / 生成窗口中的打孔带 UI 项。
/// </summary>
namespace AibisDream.FixSystem
{
public class PunchTapeItem : MonoBehaviour
{
public Image clueImageUI;
public LocalizeStringEvent nameText;
public LocalizeStringEvent memoryIndexLabelText;
public TMP_Text visibilityText;
public TMP_Text contentIdText;
public TMP_Text timecodeText;
[HideInInspector]
public bool used = false;
private PunchTape punchTapeData;
private PunchTapeDefinition punchTapeDefinition;
private void OnEnable()
{
LocalizationSettings.SelectedLocaleChanged += HandleSelectedLocaleChanged;
}
private void OnDisable()
{
LocalizationSettings.SelectedLocaleChanged -= HandleSelectedLocaleChanged;
}
public void Setup(PunchTape punchTape, PunchTapeDefinition definition)
{
punchTapeData = punchTape;
punchTapeDefinition = definition;
used = punchTape != null && punchTape.used;
ApplyDisplay(punchTapeData, definition);
}
private void ApplyDisplay(PunchTape pt, PunchTapeDefinition definition)
{
if (pt == null || definition == null)
{
Debug.LogWarning("PunchTape 或其定义为空,显示已中止。", this);
return;
}
if (clueImageUI != null)
clueImageUI.color = pt.used ? Color.grey : Color.white;
if (nameText != null)
{
RefreshLocalizedName(LocalizationKit.GetLocaleKind(
LocalizationSettings.SelectedLocale?.Identifier.Code));
}
memoryIndexLabelText?.RefreshString();
if (visibilityText != null)
visibilityText.text = $"@{definition.VisibilityCode}";
if (contentIdText != null)
contentIdText.text = $"CID: {definition.ContentId}";
if (timecodeText != null)
timecodeText.text = $"T+: {definition.Timecode}";
}
private void HandleSelectedLocaleChanged(Locale locale)
{
RefreshLocalizedName(LocalizationKit.GetLocaleKind(locale?.Identifier.Code));
}
private void RefreshLocalizedName(LocaleKind localeKind)
{
if (nameText == null || punchTapeDefinition == null)
return;
nameText.StringReference.Arguments = new object[]
{
LocalizationKit.LocalizeParam(punchTapeDefinition.DisplayNameParam, localeKind)
};
nameText.RefreshString();
}
public PunchTape GetPunchTapeData()
{
return punchTapeData;
}
}
}
@@ -14,6 +14,9 @@ namespace AibisDream.FixSystem
{
public GameObject punchTapecluePrefab;
[SerializeField] private PunchTapeCatalog punchTapeCatalog;
public PunchTapeCatalog Catalog => punchTapeCatalog;
[SerializeField] private PunchTapeFavoritesPanel favoritesPanel;
public PunchTapeFavoritesPanel FavoritesPanel => favoritesPanel;
[SerializeField] private PunchTapePrinter punchTapePrinter;
@@ -35,7 +38,7 @@ namespace AibisDream.FixSystem
{
return new PunchTapeSnapshotDto
{
favorites = _favorites.Select(t => new PunchTape(t.Category, t.ClueName, t.MemoryIndex)
favorites = _favorites.Select(t => new PunchTape(t.DefinitionId)
{
used = t.used
}).ToList()
@@ -47,7 +50,19 @@ namespace AibisDream.FixSystem
_favorites.Clear();
if (dto?.favorites != null)
{
_favorites.AddRange(dto.favorites);
foreach (var tape in dto.favorites)
{
if (!TryGetDefinition(tape, out _))
{
Debug.LogWarning(
$"[PunchTapeManager] 存档中的打孔带 ID '{tape?.DefinitionId ?? "<null>"}' 无法解析,已跳过。",
this);
continue;
}
if (!_favorites.Contains(tape))
_favorites.Add(tape);
}
}
OnCollectionChanged?.Invoke();
@@ -56,6 +71,11 @@ namespace AibisDream.FixSystem
public void AddPunchTape(PunchTape punchTape)
{
if (punchTape == null) return;
if (!TryGetDefinition(punchTape, out _))
{
Debug.LogError($"[PunchTapeManager] 无法收藏未知打孔带 '{punchTape.DefinitionId}'。", this);
return;
}
if (_favorites.Contains(punchTape))
return;
@@ -94,10 +114,52 @@ namespace AibisDream.FixSystem
favoritesPanel.ClosePanel();
}
public void GeneratePunchTapeWindow(string clueName, string category, string memoryIndex,
bool directlyAddToFavorites = false)
public bool TryGetDefinition(string definitionId, out PunchTapeDefinition definition)
{
PunchTape punchTape = new PunchTape(category, clueName, memoryIndex);
definition = null;
if (punchTapeCatalog == null)
{
Debug.LogError("[PunchTapeManager] 未配置 PunchTapeCatalog。", this);
return false;
}
var catalogErrors = punchTapeCatalog.GetValidationErrors();
if (catalogErrors.Count > 0)
{
Debug.LogError(
$"[PunchTapeManager] PunchTapeCatalog 配置无效:\n{string.Join("\n", catalogErrors)}",
this);
return false;
}
if (!punchTapeCatalog.TryGetDefinition(definitionId, out definition))
{
Debug.LogError($"[PunchTapeManager] 未找到打孔带 ID '{definitionId}'。", this);
definition = null;
return false;
}
if (!definition.TryValidate(out var error))
{
Debug.LogError($"[PunchTapeManager] 打孔带 ID '{definitionId}' 无效:{error}", this);
definition = null;
return false;
}
return true;
}
public bool TryGetDefinition(PunchTape punchTape, out PunchTapeDefinition definition)
{
return TryGetDefinition(punchTape?.DefinitionId, out definition);
}
public void GeneratePunchTapeWindow(string definitionId, bool directlyAddToFavorites = false)
{
if (!TryGetDefinition(definitionId, out var definition))
return;
PunchTape punchTape = new PunchTape(definitionId);
if (directlyAddToFavorites)
{
@@ -108,8 +170,15 @@ namespace AibisDream.FixSystem
GameObject go = Instantiate(punchTapecluePrefab, favoritesPanel.transform);
PunchTapeItem item = go.GetComponent<PunchTapeItem>();
if (item == null)
{
Debug.LogError("[PunchTapeManager] 生成 Prefab 缺少 PunchTapeItem。", go);
Destroy(go);
return;
}
currentPunchTapeGO = go;
item.Setup(punchTape);
item.Setup(punchTape, definition);
}
/// <summary>
@@ -4,16 +4,15 @@ using Yarn.Unity;
namespace AibisDream
{
public static class PunchTypeYarnCommand
public static class PunchTapeYarnCommand
{
private static PunchTapeManager PunchTapeManager => PunchTapeManager.Instance;
private static PunchTapePrinter PunchTapePrinter => PunchTapeManager.Instance.PunchTapePrinter;
[YarnCommand("generate_PunchTape")]
public static void GeneratePunchTapeWindow(string clueName, string category, string memoryIndex,
bool directlyAddToFavorites = false)
public static void GeneratePunchTapeWindow(string definitionId, bool directlyAddToFavorites = false)
{
PunchTapeManager.GeneratePunchTapeWindow(clueName, category, memoryIndex, directlyAddToFavorites);
PunchTapeManager.GeneratePunchTapeWindow(definitionId, directlyAddToFavorites);
}
[YarnCommand("CollectionClue")]
@@ -1,64 +0,0 @@
using System;
/// <summary>
/// 打孔带数据:用于记忆系统的资源键与对话节点,不依赖已废弃的 Clue 体系。
/// 仅保留可序列化字段;本地化与 Yarn 键由 UI / 流程层调用 LocalizationKit 处理。
/// </summary>
namespace AibisDream.FixSystem
{
[Serializable]
public class PunchTape : IEquatable<PunchTape>
{
public string Category;
public string ClueName;
public string MemoryIndex;
public bool used;
public PunchTape()
{
}
public PunchTape(string category, string name, string memoryIndex)
{
Category = category;
ClueName = name;
MemoryIndex = memoryIndex;
}
/// <summary>用于收藏列表去重与 UI 行映射(与 Equals 一致)。</summary>
public string GetRowKey()
{
return $"{Category ?? string.Empty}\u001f{ClueName ?? string.Empty}\u001f{MemoryIndex ?? string.Empty}";
}
public bool Equals(PunchTape other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return string.Equals(Category, other.Category, StringComparison.Ordinal) &&
string.Equals(ClueName, other.ClueName, StringComparison.Ordinal) &&
string.Equals(MemoryIndex, other.MemoryIndex, StringComparison.Ordinal);
}
public override bool Equals(object obj) => Equals(obj as PunchTape);
public override int GetHashCode()
{
unchecked
{
var hc = (Category != null ? StringComparer.Ordinal.GetHashCode(Category) : 0);
hc = (hc * 397) ^ (ClueName != null ? StringComparer.Ordinal.GetHashCode(ClueName) : 0);
hc = (hc * 397) ^ (MemoryIndex != null ? StringComparer.Ordinal.GetHashCode(MemoryIndex) : 0);
return hc;
}
}
public static bool operator ==(PunchTape left, PunchTape right)
{
if (left is null) return right is null;
return left.Equals(right);
}
public static bool operator !=(PunchTape left, PunchTape right) => !(left == right);
}
}
@@ -1,51 +0,0 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Localization.Components;
using AibisDream.Framework;
/// <summary>
/// 收藏夹 / 生成窗口中的打孔带 UI 项。
/// </summary>
namespace AibisDream.FixSystem
{
public class PunchTapeItem : MonoBehaviour
{
public Image clueImageUI;
public LocalizeStringEvent nameText;
[HideInInspector]
public bool used = false;
private PunchTape punchTapeData;
public void Setup(PunchTape punchTape)
{
punchTapeData = punchTape;
used = punchTape != null && punchTape.used;
ApplyDisplay(punchTapeData);
}
private void ApplyDisplay(PunchTape pt)
{
if (pt == null)
{
Debug.LogWarning("PunchTape is null. Display aborted.");
return;
}
if (clueImageUI != null && pt.used)
clueImageUI.color = Color.grey;
if (nameText != null)
{
nameText.StringReference.Arguments = new object[] { LocalizationKit.LocalizeParam(pt.ClueName) };
nameText.RefreshString();
}
}
public PunchTape GetPunchTapeData()
{
return punchTapeData;
}
}
}