using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Content.Shared.Corvax.CCCVars; using Content.Shared.Corvax.TTS; using Content.Shared.Physics; using Robust.Client.Graphics; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; namespace Content.Client.Corvax.TTS; /// /// Plays TTS audio in world /// // ReSharper disable once InconsistentNaming public sealed class TTSSystem : EntitySystem { [Dependency] private readonly IClydeAudio _clyde = default!; [Dependency] private readonly IEntityManager _entity = default!; [Dependency] private readonly IEyeManager _eye = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly SharedPhysicsSystem _broadPhase = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private ISawmill _sawmill = default!; private float _volume = 0.0f; private readonly HashSet _currentStreams = new(); private readonly Dictionary> _entityQueues = new(); public override void Initialize() { _sawmill = Logger.GetSawmill("tts"); _cfg.OnValueChanged(CCCVars.TTSVolume, OnTtsVolumeChanged, true); SubscribeNetworkEvent(OnPlayTTS); } public override void Shutdown() { base.Shutdown(); _cfg.UnsubValueChanged(CCCVars.TTSVolume, OnTtsVolumeChanged); EndStreams(); } // Little bit of duplication logic from AudioSystem public override void FrameUpdate(float frameTime) { var streamToRemove = new HashSet(); var ourPos = _eye.CurrentEye.Position.Position; foreach (var stream in _currentStreams) { var streamUid = GetEntity(stream.Uid); if (!stream.Source.IsPlaying || !_entity.TryGetComponent(streamUid, out var meta) || Deleted(streamUid, meta) || !_entity.TryGetComponent(streamUid, out var xform)) { stream.Source.Dispose(); streamToRemove.Add(stream); continue; } var mapPos = xform.MapPosition; if (mapPos.MapId != MapId.Nullspace) { if (!stream.Source.SetPosition(mapPos.Position)) { _sawmill.Warning("Can't set position for audio stream, stop stream."); stream.Source.StopPlaying(); } } if (mapPos.MapId == _eye.CurrentMap) { var collisionMask = (int) CollisionGroup.Impassable; var sourceRelative = ourPos - mapPos.Position; var occlusion = 0f; if (sourceRelative.Length() > 0) { occlusion = _broadPhase.IntersectRayPenetration(mapPos.MapId, new CollisionRay(mapPos.Position, sourceRelative.Normalized(), collisionMask), sourceRelative.Length(), streamUid); } stream.Source.SetOcclusion(occlusion); } } foreach (var audioStream in streamToRemove) { _currentStreams.Remove(audioStream); ProcessEntityQueue(GetEntity(audioStream.Uid)); } } private void OnTtsVolumeChanged(float volume) { _volume = volume; } private void OnPlayTTS(PlayTTSEvent ev) { var volume = _volume; if (ev.IsWhisper) volume -= 4; if (!TryCreateAudioSource(ev.Data, volume, out var source)) return; var stream = new AudioStream(ev.Uid, source); AddEntityStreamToQueue(stream); } public void StopAllStreams() { foreach (var stream in _currentStreams) stream.Source.StopPlaying(); } private bool TryCreateAudioSource(byte[] data, float volume, [NotNullWhen(true)] out IClydeAudioSource? source) { var dataStream = new MemoryStream(data) { Position = 0 }; var audioStream = _clyde.LoadAudioOggVorbis(dataStream); source = _clyde.CreateAudioSource(audioStream); source?.SetVolume(volume); return source != null; } private void AddEntityStreamToQueue(AudioStream stream) { var uid = GetEntity(stream.Uid); if (_entityQueues.TryGetValue(uid, out var queue)) { queue.Enqueue(stream); } else { _entityQueues.Add(uid, new Queue(new[] { stream })); if (!IsEntityCurrentlyPlayStream(stream.Uid)) ProcessEntityQueue(uid); } } private bool IsEntityCurrentlyPlayStream(NetEntity uid) { return _currentStreams.Any(s => s.Uid == uid); } private void ProcessEntityQueue(EntityUid uid) { if (TryTakeEntityStreamFromQueue(uid, out var stream)) PlayEntity(stream); } private bool TryTakeEntityStreamFromQueue(EntityUid uid, [NotNullWhen(true)] out AudioStream? stream) { if (_entityQueues.TryGetValue(uid, out var queue)) { stream = queue.Dequeue(); if (queue.Count == 0) _entityQueues.Remove(uid); return true; } stream = null; return false; } private void PlayEntity(AudioStream stream) { if (!_entity.TryGetComponent(GetEntity(stream.Uid), out var xform) || !stream.Source.SetPosition(_transform.GetWorldPosition(xform))) return; stream.Source.StartPlaying(); _currentStreams.Add(stream); } private void EndStreams() { foreach (var stream in _currentStreams) { stream.Source.StopPlaying(); stream.Source.Dispose(); } _currentStreams.Clear(); _entityQueues.Clear(); } // ReSharper disable once InconsistentNaming private sealed class AudioStream { public NetEntity Uid { get; } public IClydeAudioSource Source { get; } public AudioStream(NetEntity uid, IClydeAudioSource source) { Uid = uid; Source = source; } } }