mirror of
https://github.com/wega-team/ss14-wega.git
synced 2026-06-09 10:06:49 +02:00
153 lines
6.7 KiB
C#
153 lines
6.7 KiB
C#
using System.Threading.Tasks;
|
|
using Content.Shared.Chat;
|
|
using Content.Server.Chat.Systems;
|
|
using Content.Shared.Corvax.CCCVars;
|
|
using Content.Shared.Corvax.TTS;
|
|
using Content.Shared.GameTicking;
|
|
using Content.Shared.Players.RateLimiting;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
|
|
namespace Content.Server.Corvax.TTS;
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
public sealed partial class TTSSystem : EntitySystem
|
|
{
|
|
[Dependency] private IConfigurationManager _cfg = default!;
|
|
[Dependency] private IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private TTSManager _ttsManager = default!;
|
|
[Dependency] private SharedTransformSystem _xforms = default!;
|
|
[Dependency] private IRobustRandom _rng = default!;
|
|
|
|
private readonly List<string> _sampleText =
|
|
new()
|
|
{
|
|
"Съешь же ещё этих мягких французских булок, да выпей чаю.",
|
|
"Клоун, прекрати разбрасывать банановые кожурки офицерам под ноги!",
|
|
"Капитан, вы уверены что хотите назначить клоуна на должность главы персонала?",
|
|
"Эс Бэ! Тут человек в сером костюме, с тулбоксом и в маске! Помогите!!",
|
|
"Учёные, тут странная аномалия в баре! Она уже съела мима!",
|
|
"Я надеюсь что инженеры внимательно следят за сингулярностью...",
|
|
"Вы слышали эти странные крики в техах? Мне кажется туда ходить небезопасно.",
|
|
"Вы не видели Гамлета? Мне кажется он забегал к вам на кухню.",
|
|
"Здесь есть доктор? Человек умирает от отравленного пончика! Нужна помощь!",
|
|
"Вам нужно согласие и печать квартирмейстера, если вы хотите сделать заказ на партию дробовиков.",
|
|
"Возле эвакуационного шаттла разгерметизация! Инженеры, нам срочно нужна ваша помощь!",
|
|
"Бармен, налей мне самого крепкого вина, которое есть в твоих запасах!"
|
|
};
|
|
|
|
private const int MaxMessageChars = 100 * 2; // same as SingleBubbleCharLimit * 2
|
|
private bool _isEnabled = false;
|
|
|
|
public override void Initialize()
|
|
{
|
|
_cfg.OnValueChanged(CCCVars.TTSEnabled, v => _isEnabled = v, true);
|
|
|
|
SubscribeLocalEvent<TransformSpeechEvent>(OnTransformSpeech);
|
|
SubscribeLocalEvent<TTSComponent, EntitySpokeEvent>(OnEntitySpoke);
|
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
|
|
|
SubscribeNetworkEvent<RequestPreviewTTSEvent>(OnRequestPreviewTTS);
|
|
|
|
RegisterRateLimits();
|
|
}
|
|
|
|
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
|
|
{
|
|
_ttsManager.ResetCache();
|
|
}
|
|
|
|
private async void OnRequestPreviewTTS(RequestPreviewTTSEvent ev, EntitySessionEventArgs args)
|
|
{
|
|
if (!_isEnabled ||
|
|
!_prototypeManager.TryIndex<TTSVoicePrototype>(ev.VoiceId, out var protoVoice))
|
|
return;
|
|
|
|
if (HandleRateLimit(args.SenderSession) != RateLimitStatus.Allowed)
|
|
return;
|
|
|
|
var previewText = _rng.Pick(_sampleText);
|
|
var soundData = await GenerateTTS(previewText, protoVoice.Speaker);
|
|
if (soundData is null)
|
|
return;
|
|
|
|
RaiseNetworkEvent(new PlayTTSEvent(soundData), Filter.SinglePlayer(args.SenderSession));
|
|
}
|
|
|
|
private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySpokeEvent args)
|
|
{
|
|
var voiceId = component.VoicePrototypeId;
|
|
if (!_isEnabled ||
|
|
args.Message.Length > MaxMessageChars ||
|
|
string.IsNullOrEmpty(voiceId))
|
|
return;
|
|
|
|
var voiceEv = new TransformSpeakerVoiceEvent(uid, voiceId);
|
|
RaiseLocalEvent(uid, voiceEv);
|
|
voiceId = voiceEv.VoiceId;
|
|
|
|
if (!_prototypeManager.TryIndex<TTSVoicePrototype>(voiceId, out var protoVoice))
|
|
return;
|
|
|
|
if (args.ObfuscatedMessage != null)
|
|
{
|
|
HandleWhisper(uid, args.Message, args.ObfuscatedMessage, protoVoice.Speaker);
|
|
return;
|
|
}
|
|
|
|
HandleSay(uid, args.Message, protoVoice.Speaker);
|
|
}
|
|
|
|
private async void HandleSay(EntityUid uid, string message, string speaker)
|
|
{
|
|
var soundData = await GenerateTTS(message, speaker);
|
|
if (soundData is null) return;
|
|
RaiseNetworkEvent(new PlayTTSEvent(soundData, GetNetEntity(uid)), Filter.Pvs(uid));
|
|
}
|
|
|
|
private async void HandleWhisper(EntityUid uid, string message, string obfMessage, string speaker)
|
|
{
|
|
var fullSoundData = await GenerateTTS(message, speaker, true);
|
|
if (fullSoundData is null) return;
|
|
|
|
var obfSoundData = await GenerateTTS(obfMessage, speaker, true);
|
|
if (obfSoundData is null) return;
|
|
|
|
var fullTtsEvent = new PlayTTSEvent(fullSoundData, GetNetEntity(uid), true);
|
|
var obfTtsEvent = new PlayTTSEvent(obfSoundData, GetNetEntity(uid), true);
|
|
|
|
// TODO: Check obstacles
|
|
var xformQuery = GetEntityQuery<TransformComponent>();
|
|
var sourcePos = _xforms.GetWorldPosition(xformQuery.GetComponent(uid), xformQuery);
|
|
var receptions = Filter.Pvs(uid).Recipients;
|
|
foreach (var session in receptions)
|
|
{
|
|
if (!session.AttachedEntity.HasValue) continue;
|
|
var xform = xformQuery.GetComponent(session.AttachedEntity.Value);
|
|
var distance = (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).Length();
|
|
if (distance > ChatSystem.VoiceRange)
|
|
continue;
|
|
|
|
RaiseNetworkEvent(distance > ChatSystem.WhisperClearRange ? obfTtsEvent : fullTtsEvent, session);
|
|
}
|
|
}
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
private async Task<byte[]?> GenerateTTS(string text, string speaker, bool isWhisper = false)
|
|
{
|
|
var textSanitized = Sanitize(text);
|
|
if (textSanitized == "") return null;
|
|
if (char.IsLetter(textSanitized[^1]))
|
|
textSanitized += ".";
|
|
|
|
var ssmlTraits = SoundTraits.RateFast;
|
|
if (isWhisper)
|
|
ssmlTraits = SoundTraits.PitchVerylow;
|
|
var textSsml = ToSsmlText(textSanitized, ssmlTraits);
|
|
|
|
return await _ttsManager.ConvertTextToSpeech(speaker, textSsml);
|
|
}
|
|
}
|