Merge branch 'develop' into 7.20春优化

This commit is contained in:
2026-07-21 01:39:29 +08:00
14 changed files with 1921 additions and 1694 deletions
+123
View File
@@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using AibisDream.Utility;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Yarn.Markup;
using Yarn.Unity;
namespace AibisDream.SystemEditor.Tests
{
public sealed class AutoNextMetadataTests
{
[Test]
public void AutoNextWithoutParameter_UsesExistingFixedDelay()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
}
[Test]
public void AutoNextWithParameter_UsesSpecifiedSeconds()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
}
[Test]
public void AutoNextWithZeroDelay_IsValid()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next:0"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(0f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.Zero);
}
[Test]
public void AutoNextParameter_UsesInvariantCulture()
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[TestCase("auto_next:")]
[TestCase("auto_next:abc")]
[TestCase("auto_next:-1")]
[TestCase("auto_next:NaN")]
[TestCase("auto_next:Infinity")]
[TestCase("auto_next:2147484")]
public void AutoNextWithInvalidParameter_FallsBackToExistingFixedDelay(string metadata)
{
LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效"));
var lineInfo = LineInfo.Generate(CreateLine(metadata));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
}
[TestCase("auto_next_extra")]
[TestCase("auto_next_extra:4.5")]
[TestCase("AUTO_NEXT")]
[TestCase("AUTO_NEXT:4.5")]
public void SimilarMetadata_IsNotRecognized(string metadata)
{
var line = CreateLine(metadata);
Assert.That(line.IsAutoSkipLine(), Is.False);
Assert.That(line.TryGetAutoNextDelaySeconds(out _), Is.False);
}
[Test]
public void ParameterizedTag_TakesPrecedenceOverPlainTag()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next", "auto_next:2.5"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
}
[Test]
public void MultipleParameterizedTags_UseFirstAndWarn()
{
LogAssert.Expect(LogType.Warning, new Regex("存在多个 auto_next 参数标签"));
var lineInfo = LineInfo.Generate(CreateLine("auto_next:2.5", "auto_next:4.5"));
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
}
private static LocalizedLine CreateLine(params string[] metadata)
{
return new LocalizedLine
{
TextID = "line:auto-next-test",
Metadata = metadata,
Text = new MarkupParseResult("Test line", new List<MarkupAttribute>())
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d476e493adf4855468ce46409dae6661
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -37,7 +37,7 @@ ScriptedImporter:
sRGBTexture: 1
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
spriteMeshType: 0
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 43
@@ -37,7 +37,7 @@ ScriptedImporter:
sRGBTexture: 1
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
spriteMeshType: 0
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 43
@@ -286,7 +286,7 @@ ScriptedImporter:
- name: DefaultTexturePlatform
overridden: 0
ignorePlatformSupport: 0
maxTextureSize: 4096
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
@@ -516,7 +516,7 @@ ScriptedImporter:
sharedRigPSDLayers: []
pSDLayerImportSetting: []
importFileNodeState: 1
platformSettingsDirtyTick: 639100691398813597
platformSettingsDirtyTick: 639201850035897518
spriteSizeExpandChanged: 0
generateGOHierarchy: 0
textureAssetName:
@@ -706,7 +706,7 @@ MonoBehaviour:
director: {fileID: 6948339993923372708}
autoBindCinemachineBrain: 1
resetToStartWhenFinished: 1
resetToStartDelay: 0.5
resetToStartDelay: 1
--- !u!1 &2615926060114227568
GameObject:
m_ObjectHideFlags: 0
@@ -1359,8 +1359,8 @@ MonoBehaviour:
m_ShadowVolumeIntensityEnabled: 0
m_ShadowVolumeIntensity: 0.75
m_LocalBounds:
m_Center: {x: 0.03494978, y: -0.07005, z: 0}
m_Extent: {x: 14.116449, y: 5.57125, z: 0}
m_Center: {x: 0.028900146, y: -0.069649935, z: 0}
m_Extent: {x: 14.1065, y: 5.5717497, z: 0}
m_PointLightInnerAngle: 360
m_PointLightOuterAngle: 360
m_PointLightInnerRadius: 0
@@ -1412,58 +1412,6 @@ Transform:
- {fileID: 6274499368345295635}
m_Father: {fileID: 9073824794961298310}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &9102307160016725310
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9102307160016725311}
- component: {fileID: 9102307160016725312}
m_Layer: 0
m_Name: Actor Entrance
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &9102307160016725311
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9102307160016725310}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -1.55, y: 0.15, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9111904319609796187}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &9102307160016725312
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9102307160016725310}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbc0dde0e8b8a9c498545e1b5b508241, type: 3}
m_Name:
m_EditorClassIdentifier:
data:
slotPosition: {x: 0, y: 0, z: 0}
actorRole: 2
idx: 1
maxWidth: 800
minWidth: 300
bubbleStyle: {fileID: 11400000, guid: e3f425b45fd35cc4caf1939eaea2f7bc, type: 2}
pivotType: 5
--- !u!1 &5345859371557614914
GameObject:
m_ObjectHideFlags: 0
@@ -1727,7 +1675,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: ee95229f127bf4c45ab49816cd2ea8c8, type: 3}
m_Name:
m_EditorClassIdentifier:
bubbleSlotGroup: {fileID: 0}
--- !u!1 &5756145678206075836
GameObject:
m_ObjectHideFlags: 0
@@ -2652,7 +2599,7 @@ Transform:
m_GameObject: {fileID: 8792714536426500327}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -2.6, y: -5, z: 0}
m_LocalPosition: {x: -2.7, y: -5, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -2762,3 +2709,55 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
cameraType: 2
--- !u!1 &9102307160016725310
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9102307160016725311}
- component: {fileID: 9102307160016725312}
m_Layer: 0
m_Name: Actor Entrance
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &9102307160016725311
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9102307160016725310}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -1.55, y: 0.15, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9111904319609796187}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &9102307160016725312
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9102307160016725310}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbc0dde0e8b8a9c498545e1b5b508241, type: 3}
m_Name:
m_EditorClassIdentifier:
data:
slotPosition: {x: 0, y: 0, z: 0}
actorRole: 2
idx: 1
maxWidth: 800
minWidth: 300
bubbleStyle: {fileID: 11400000, guid: e3f425b45fd35cc4caf1939eaea2f7bc, type: 2}
pivotType: 5
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -9577,10 +9577,18 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5424929553171325091, guid: 46012f47c455f2a439c777c08b1f5393, type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7548078866038160482, guid: 46012f47c455f2a439c777c08b1f5393, type: 3}
propertyPath: m_Name
value: Play Tool Panel
objectReference: {fileID: 0}
- target: {fileID: 7715856805303343576, guid: 46012f47c455f2a439c777c08b1f5393, type: 3}
propertyPath: m_Color.a
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
+22 -2
View File
@@ -156,16 +156,24 @@ namespace AibisDream
public string lineText;
public CharacterVo character;
public bool isAutoSkip;
public float? autoNextDelaySeconds;
public bool isOption;
public static LineInfo Generate(LocalizedLine dialogueLine)
{
float? autoNextDelaySeconds = null;
if (dialogueLine.TryGetAutoNextDelaySeconds(out var parsedDelaySeconds))
{
autoNextDelaySeconds = parsedDelaySeconds;
}
return new LineInfo
{
lineId = dialogueLine.TextID,
lineText = dialogueLine.TextWithoutCharacterName.Text,
character = dialogueLine.GetCharacterVo(),
isAutoSkip = dialogueLine.IsAutoSkipLine(),
autoNextDelaySeconds = autoNextDelaySeconds,
isOption = false
};
}
@@ -187,6 +195,18 @@ namespace AibisDream
{
return ConstRef.FixedDelay;
}
public int CalcAutoNextDelayTime()
{
if (!autoNextDelaySeconds.HasValue)
{
return ConstRef.FixedDelay;
}
return (int)Math.Round(
autoNextDelaySeconds.Value * 1000d,
MidpointRounding.AwayFromZero);
}
}
public interface ITextShown
@@ -220,7 +240,7 @@ namespace AibisDream
try
{
token.state = LineSyncState.AdvanceDelay;
await Task.Delay(token.lineInfo.CalcDelayTime(), token.AdvanceDelayToken.Token);
await Task.Delay(token.lineInfo.CalcAutoNextDelayTime(), token.AdvanceDelayToken.Token);
token.state = LineSyncState.WaitForAutoAdvance;
token.TryAdvance();
}
@@ -251,4 +271,4 @@ namespace AibisDream
// 仅限DialogController使用
InOption
}
}
}
+63 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using AibisDream.Kit;
using Yarn.Markup;
@@ -10,6 +11,7 @@ namespace AibisDream.Utility
public static class YarnUtil
{
private const string AutoNext = "auto_next";
private const string AutoNextParameterPrefix = AutoNext + ":";
public const string OptionPrompt = "option_prompt";
#region
@@ -21,7 +23,67 @@ namespace AibisDream.Utility
/// <returns>是否应该自动跳过</returns>
public static bool IsAutoSkipLine(this LocalizedLine line)
{
return line.Metadata != null && line.Metadata.Contains(AutoNext);
return line?.Metadata != null && line.Metadata.Any(IsAutoNextMetadata);
}
/// <summary>
/// 尝试读取 auto_next 标签中以秒为单位的停留时间。
/// </summary>
/// <remarks>
/// 无参数的 auto_next 返回 false;无效参数会输出警告并由调用方回退到默认延时。
/// 如果一行存在多个参数标签,则使用第一个。
/// </remarks>
public static bool TryGetAutoNextDelaySeconds(this LocalizedLine line, out float delaySeconds)
{
delaySeconds = default;
if (line?.Metadata == null)
{
return false;
}
var parameterizedTags = line.Metadata
.Where(metadata => metadata != null &&
metadata.StartsWith(AutoNextParameterPrefix, StringComparison.Ordinal))
.ToArray();
if (parameterizedTags.Length == 0)
{
return false;
}
if (parameterizedTags.Length > 1)
{
UnityEngine.Debug.LogWarning(
$"[YarnUtil] 行 {line.TextID} 存在多个 auto_next 参数标签,将使用第一个:{parameterizedTags[0]}");
}
var parameterizedTag = parameterizedTags[0];
var rawValue = parameterizedTag.Substring(AutoNextParameterPrefix.Length);
var isValid = float.TryParse(
rawValue,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out delaySeconds)
&& !float.IsNaN(delaySeconds)
&& !float.IsInfinity(delaySeconds)
&& delaySeconds >= 0f
&& delaySeconds * 1000d <= int.MaxValue;
if (isValid)
{
return true;
}
UnityEngine.Debug.LogWarning(
$"[YarnUtil] 行 {line.TextID} 的 auto_next 参数无效:{parameterizedTag},将使用默认延时。");
delaySeconds = default;
return false;
}
private static bool IsAutoNextMetadata(string metadata)
{
return string.Equals(metadata, AutoNext, StringComparison.Ordinal)
|| (metadata != null && metadata.StartsWith(AutoNextParameterPrefix, StringComparison.Ordinal));
}
/// <summary>
+16 -16
View File
@@ -12,29 +12,29 @@ Task: 分析病理与治疗方案 #line:0e94a4a
me: 还好…看起来温度还没上来。我们还有一点时间…… #line:04dbddd
<<fade_in 1.5>>
<<wait 0.6>>
<<show_obj "pp2_分析0-0" 0>>
<<show_obj "pp2_分析0-1" 0>>
<<show_obj "pp2_分析0-2" 0>>
<<show_full_screen "pp2_分析0-0" 0>>
<<show_full_screen "pp2_分析0-1" 0>>
<<show_full_screen "pp2_分析0-2" 0>>
<<wait 0.4>>
me: 刚才我们看到的…就是UF的学习过程。<br>UF正在利用这只眼睛进行“自动训练”。 #line:0085043
me: 这个自动进程被持续地干扰了,<br>形成了死循环,因此造成发热和头痛。 #line:0353277
<<wait 1>>
<<show_obj "pp2_分析1" 0>>
<<show_full_screen "pp2_分析1" 0>>
me: 其根本原因是…<br>[[size d=0.1]]原<waitfor=0.05>型<waitfor=0.1>排<waitfor=0.05>异<waitfor=0.1>反<waitfor=0.05>应[[/size]]。
pp: 怎么又是原型?
<<wait 0.5>>
<<show_obj "pp2_分析2-1" 0>>
<<show_obj "pp2_分析2-2" 0>>
<<show_full_screen "pp2_分析2-1" 0>>
<<show_full_screen "pp2_分析2-2" 0>>
me: 出于某种原因这种排异反应在今天之内加重了。这导致了记忆模块的进一步数据泄露。
me: 当这种数据请求逐渐累积,而既有的线路又不再存在了… #line:06801c4
<<wait 0.5>>
<<show_obj "pp2_分析3-1" 0>>
<<show_obj "pp2_分析3-2" 0>>
<<show_full_screen "pp2_分析3-1" 0>>
<<show_full_screen "pp2_分析3-2" 0>>
me: 这些数据就被“挤”入了另一条记忆-视觉桥。也就是被UF占据的这只眼睛里。 #line:0a6a9e6
@@ -48,15 +48,15 @@ me: 在失去最后看见色彩的能力之后…你变得更想*看见*了,
peipei: 看见? #line:004cafe
me: 看见色彩。<br>虽然你表现得似乎无所谓…但你更加无法抑制住地…想要去看见色彩了。对吧? #line:06d3d87
peipei: …… #line:013c17a
<<show_obj "pp2_分析4-1" 0>>
<<show_full_screen "pp2_分析4-1" 0>>
<<wait 0.5>>
<<show_obj "pp2_分析4-2" 0>>
<<show_obj "pp2_分析4-3" 0>>
<<show_obj "pp2_分析4-4" 0>>
<<show_obj "pp2_分析4-5" 0>>
<<show_obj "pp2_分析4-6" 0>>
<<show_full_screen "pp2_分析4-2" 0>>
<<show_full_screen "pp2_分析4-3" 0>>
<<show_full_screen "pp2_分析4-4" 0>>
<<show_full_screen "pp2_分析4-5" 0>>
<<show_full_screen "pp2_分析4-6" 0>>
<<wait 1>>
<<show_obj "pp2_分析4-7" 0>>
<<show_full_screen "pp2_分析4-7" 0>>
me: 我想这就是原因。
me: 原型是一种很复杂的系统,有时越是去尝试阻断…就反而越活跃了。 #line:0722937
@@ -82,7 +82,7 @@ me: 世界本来就是黑白的,夕阳、绿叶……本来就是灰色的。
<<hide_dialog>>
<<wait 1>>
me: 这可能是…唯一的方案了。 #line:068d2a0
<<hide_obj>>
<<hide_full_screem>>
peipei: ……<br>为什么…<br>…… #line:0372b04
<<fade_in 1>>
<<jump 结束分析>>
@@ -86,7 +86,7 @@ position: 248,443
<<fade_in 0>>
<<wait 1.5>>
CenterText: 第一章<waitfor=4> #auto_next #line:0bf67b0
CenterText: 第一章 #auto_next:4 #line:0bf67b0
<<change_amb Subway>>
<<wait 2>>