using System.Threading.Tasks; using Content.Server.Chat.Systems; using Content.Shared.Corvax.CCCVars; using Content.Shared.Corvax.TTS; using Content.Shared.GameTicking; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Server.Corvax.TTS; // ReSharper disable once InconsistentNaming public sealed partial class TTSSystem : EntitySystem { [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IServerNetManager _netMgr = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly TTSManager _ttsManager = default!; [Dependency] private readonly SharedTransformSystem _xforms = default!; 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(OnTransformSpeech); SubscribeLocalEvent(OnEntitySpoke); SubscribeLocalEvent(OnRoundRestartCleanup); _netMgr.RegisterNetMessage(OnRequestTTS); } private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev) { _ttsManager.ResetCache(); } private async void OnRequestTTS(MsgRequestTTS ev) { if (!_isEnabled || ev.Text.Length > MaxMessageChars || !_playerManager.TryGetSessionByChannel(ev.MsgChannel, out var session) || !_prototypeManager.TryIndex(ev.VoiceId, out var protoVoice)) return; var soundData = await GenerateTTS(ev.Text, protoVoice.Speaker); if (soundData is null) return; RaiseNetworkEvent(new PlayTTSEvent(GetNetEntity(ev.Uid), soundData), Filter.SinglePlayer(session)); } private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySpokeEvent args) { var voiceId = component.VoicePrototypeId; if (!_isEnabled || args.Message.Length > MaxMessageChars || voiceId == null) return; var voiceEv = new TransformSpeakerVoiceEvent(uid, voiceId); RaiseLocalEvent(uid, voiceEv); voiceId = voiceEv.VoiceId; if (!_prototypeManager.TryIndex(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(GetNetEntity(uid), soundData), 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(GetNetEntity(uid), fullSoundData, true); var obfTtsEvent = new PlayTTSEvent(GetNetEntity(uid), obfSoundData, true); // TODO: Check obstacles var xformQuery = GetEntityQuery(); 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 * ChatSystem.VoiceRange) continue; RaiseNetworkEvent(distance > ChatSystem.WhisperClearRange ? obfTtsEvent : fullTtsEvent, session); } } // ReSharper disable once InconsistentNaming private async Task 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); } } public sealed class TransformSpeakerVoiceEvent : EntityEventArgs { public EntityUid Sender; public string VoiceId; public TransformSpeakerVoiceEvent(EntityUid sender, string voiceId) { Sender = sender; VoiceId = voiceId; } }