-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathQrtAudioService.cs
More file actions
274 lines (237 loc) · 9.87 KB
/
Copy pathQrtAudioService.cs
File metadata and controls
274 lines (237 loc) · 9.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
using System;
using System.IO;
using System.Reflection;
using System.Text;
using AudioManagerAPI.Defaults;
using AudioManagerAPI.Features.Enums;
using LabApi.Features.Wrappers;
using UnityEngine;
using LabAnnouncer = LabApi.Features.Wrappers.Announcer;
using Logger = LabApi.Features.Console.Logger;
namespace QRTForces
{
public sealed class QrtAudioService
{
private const string LogPrefix = "[QRTForces]";
private const string CassieResourceName = "QRTForces.Audio.cassie.wav";
private const string EntryResourceName = "QRTForces.Audio.spawn.wav";
private const float FallbackCassieSubtitleDurationSeconds = 20f;
private const int MaximumCassieSubtitleDurationSeconds = 300;
private readonly QrtConfig config;
private readonly QrtTranslation translation;
private bool audioRegistered;
private bool cassieAudioAvailable;
private bool entryAudioAvailable;
private int cassieSessionId;
private int entrySessionId;
public QrtAudioService(QrtConfig config, QrtTranslation translation)
{
this.config = config ?? QrtConfig.CreateDefault();
this.translation = translation ?? QrtTranslation.CreateDefault();
}
public void RegisterAudioResources()
{
if (audioRegistered)
{
return;
}
audioRegistered = true;
cassieAudioAvailable = TryRegisterAudioResource(config.Audio.CassieAudioKey, CassieResourceName, "CASSIE 公告音频");
entryAudioAvailable = TryRegisterAudioResource(config.Audio.EntryAudioKey, EntryResourceName, "快速反应部队入场音频");
}
public void PlaySummonAnnouncement(Func<Player, bool> qrtMemberFilter)
{
PlayCassieSubtitle();
PlayCassieAudio();
PlayEntryAudio(qrtMemberFilter);
}
public void StopAll(string reason)
{
StopSession(ref cassieSessionId, $"CASSIE 公告音频,原因:{reason}");
StopSession(ref entrySessionId, $"快速反应部队入场音频,原因:{reason}");
}
private bool TryRegisterAudioResource(string rawKey, string resourceName, string purpose)
{
string key = NormalizeKey(rawKey);
if (string.IsNullOrWhiteSpace(key))
{
Logger.Warn($"{LogPrefix} {purpose} key 为空,已跳过嵌入资源注册。");
return false;
}
using (Stream stream = OpenEmbeddedResource(resourceName))
{
if (stream == null)
{
Logger.Warn($"{LogPrefix} {purpose}嵌入资源缺失:{resourceName},播放时会跳过。");
return false;
}
}
try
{
DefaultAudioManager.RegisterAudio(key, () => OpenEmbeddedResource(resourceName));
Logger.Info($"{LogPrefix} 已注册{purpose}:key={key},resource={resourceName}。");
return true;
}
catch (Exception exception)
{
Logger.Warn($"{LogPrefix} 注册{purpose}失败:key={key},resource={resourceName}。播放时仍会尝试使用该 key,错误:{exception.Message}");
return true;
}
}
private void PlayCassieSubtitle()
{
if (string.IsNullOrWhiteSpace(translation.CassieAnnouncementText))
{
Logger.Info($"{LogPrefix} CassieAnnouncementText 为空,已跳过 CASSIE 文本和字幕。");
return;
}
try
{
int subtitleDurationSeconds = ResolveCassieSubtitleDurationSeconds(
config.Audio.CassieSubtitleMinimumDurationSeconds);
string timingText = BuildCassieSubtitleTimingText(subtitleDurationSeconds);
LabAnnouncer.Message(
timingText,
translation.CassieAnnouncementText.Trim(),
false,
0f,
0f);
Logger.Info(
$"{LogPrefix} 已发送快速反应部队 CASSIE 字幕公告,英文句号数={subtitleDurationSeconds}," +
$"预计显示约 {subtitleDurationSeconds} 秒,配置值={config.Audio.CassieSubtitleMinimumDurationSeconds:0.###} 秒;字幕允许先于音频结束。");
}
catch (Exception exception)
{
Logger.Warn($"{LogPrefix} 发送快速反应部队 CASSIE 字幕公告失败,召唤流程不回滚。错误:{exception.Message}");
}
}
internal static int ResolveCassieSubtitleDurationSeconds(float configuredSeconds)
{
float safeSeconds = IsFinite(configuredSeconds)
? Math.Min(MaximumCassieSubtitleDurationSeconds, Math.Max(1f, configuredSeconds))
: FallbackCassieSubtitleDurationSeconds;
return (int)Math.Ceiling(safeSeconds);
}
internal static string BuildCassieSubtitleTimingText(int durationSeconds)
{
int periodCount = Math.Min(MaximumCassieSubtitleDurationSeconds, Math.Max(1, durationSeconds));
StringBuilder timingText = new StringBuilder((periodCount * 2) - 1);
for (int i = 0; i < periodCount; i++)
{
if (i > 0)
{
timingText.Append(' ');
}
timingText.Append('.');
}
return timingText.ToString();
}
private void PlayCassieAudio()
{
string key = NormalizeKey(config.Audio.CassieAudioKey);
if (string.IsNullOrWhiteSpace(key))
{
Logger.Warn($"{LogPrefix} Audio.CassieAudioKey 为空,已跳过 CASSIE 公告音频播放。");
return;
}
if (!cassieAudioAvailable)
{
Logger.Warn($"{LogPrefix} CASSIE 公告音频未成功注册,已跳过播放。key={key}");
return;
}
StopSession(ref cassieSessionId, "替换旧 CASSIE 公告音频");
cassieSessionId = PlayGlobalAudio(
key,
config.Audio.CassieVolume,
target => target != null,
"CASSIE 公告音频");
}
private void PlayEntryAudio(Func<Player, bool> qrtMemberFilter)
{
string key = NormalizeKey(config.Audio.EntryAudioKey);
if (string.IsNullOrWhiteSpace(key))
{
Logger.Warn($"{LogPrefix} Audio.EntryAudioKey 为空,已跳过快速反应部队入场音频播放。");
return;
}
if (!entryAudioAvailable)
{
Logger.Warn($"{LogPrefix} 快速反应部队入场音频未成功注册,已跳过播放。key={key}");
return;
}
if (qrtMemberFilter == null)
{
Logger.Warn($"{LogPrefix} 快速反应部队入场音频缺少成员过滤器,已跳过播放。");
return;
}
StopSession(ref entrySessionId, "替换旧快速反应部队入场音频");
entrySessionId = PlayGlobalAudio(
key,
config.Audio.EntryVolume,
target => target != null && qrtMemberFilter(target),
"快速反应部队入场音频");
}
private int PlayGlobalAudio(string key, float volume, Func<Player, bool> filter, string purpose)
{
try
{
int sessionId = DefaultAudioManager.Instance.PlayGlobalAudio(
key: key,
loop: false,
volume: Mathf.Clamp01(volume),
priority: AudioPriority.High,
validPlayersFilter: filter,
queue: false,
fadeInDuration: 0f,
persistent: false,
lifespan: null,
autoCleanup: true);
if (sessionId == 0)
{
Logger.Warn($"{LogPrefix} 播放{purpose}失败:AudioManagerAPI 返回 sessionId=0。key={key}");
return 0;
}
Logger.Info($"{LogPrefix} 已播放{purpose}:key={key},sessionId={sessionId}。");
return sessionId;
}
catch (Exception exception)
{
Logger.Warn($"{LogPrefix} 播放{purpose}失败,召唤流程不回滚。key={key},错误:{exception.Message}");
return 0;
}
}
private void StopSession(ref int sessionId, string purpose)
{
if (sessionId <= 0)
{
return;
}
int stoppedSessionId = sessionId;
try
{
DefaultAudioManager.Stop(stoppedSessionId);
Logger.Info($"{LogPrefix} 已停止{purpose}。sessionId={stoppedSessionId}");
}
catch (Exception exception)
{
Logger.Warn($"{LogPrefix} 停止{purpose}失败。sessionId={stoppedSessionId},错误:{exception.Message}");
}
finally
{
sessionId = 0;
}
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
private static Stream OpenEmbeddedResource(string resourceName)
{
return Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
}
private static string NormalizeKey(string key)
{
return string.IsNullOrWhiteSpace(key) ? string.Empty : key.Trim();
}
}
}