diff --git a/Content.Client/_Art/TTS/TTSSystem.cs b/Content.Client/_Art/TTS/TTSSystem.cs index 40ac4842fd8..210ee3cf1e9 100644 --- a/Content.Client/_Art/TTS/TTSSystem.cs +++ b/Content.Client/_Art/TTS/TTSSystem.cs @@ -17,11 +17,11 @@ namespace Content.Client._Art.TTS; /// Plays TTS audio in world /// // ReSharper disable once InconsistentNaming -public sealed class TTSSystem : EntitySystem +public sealed partial class TTSSystem : EntitySystem { - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IResourceManager _res = default!; - [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private IConfigurationManager _cfg = default!; + [Dependency] private IResourceManager _res = default!; + [Dependency] private AudioSystem _audio = default!; private ISawmill _sawmill = default!; private readonly MemoryContentRoot _contentRoot = new(); diff --git a/Content.Goobstation.Client/Barks/BarkSystem.cs b/Content.Goobstation.Client/Barks/BarkSystem.cs index 63ec6ab3d18..9b2a0995197 100644 --- a/Content.Goobstation.Client/Barks/BarkSystem.cs +++ b/Content.Goobstation.Client/Barks/BarkSystem.cs @@ -8,6 +8,10 @@ using Robust.Shared.Timing; using Content.Goobstation.Common.CCVar; using Content.Shared._Arcane.CCVars; +// Arcane-Start +using Content.Shared.CCVar; +using Content.Client.Audio; +// Arcane-End namespace Content.Goobstation.Client.Barks; @@ -48,6 +52,18 @@ public void OnPreviewBark(PreviewBarkEvent ev) private void OnPlayBark(PlayBarkEvent ev) { var sourceEntity = GetEntity(ev.SourceUid); + + // Arcane-Start - Radio bark: the speaker is not in the listener's PVS, so the bark prototype travels with the event. + if (ev.BarkProtoId is { } protoId) + { + if (!_prototypeManager.TryIndex(protoId, out var barkProto)) + return; + + PlayBark(sourceEntity, ev.Message, ev.Whisper, barkProto, radio: true); + return; + } + // Arcane-End + if (!TryComp(sourceEntity, out var comp) || comp.VoicePrototypeId is null || !_prototypeManager.TryIndex(comp.VoicePrototypeId, out var proto)) @@ -56,7 +72,7 @@ private void OnPlayBark(PlayBarkEvent ev) PlayBark(sourceEntity, ev.Message, ev.Whisper, proto); } - private void PlayBark(EntityUid? source, string message, bool whisper, BarkPrototype proto) + private void PlayBark(EntityUid? source, string message, bool whisper, BarkPrototype proto, bool radio = false) // Arcane-Edit { // Arcane-start if (_cfg.GetCVar(ACCVars.UseTTS)) @@ -69,7 +85,7 @@ private void PlayBark(EntityUid? source, string message, bool whisper, BarkProto if (message.Length > 50) message = message[..50]; - var volume = GetVolume(whisper, proto); + var volume = GetVolume(whisper, proto, radio); // Arcane-Edit if (volume <= -20f) return; @@ -91,6 +107,7 @@ private void PlayBark(EntityUid? source, string message, bool whisper, BarkProto { Source = source, IsPreview = source == null, + IsRadio = radio, // Arcane Message = message, Prototype = proto, Volume = volume, @@ -113,7 +130,7 @@ public override void Update(float frameTime) if (bark.NextSound > _timing.CurTime) continue; - if (!bark.IsPreview && TerminatingOrDeleted(bark.Source!.Value)) + if (!bark.IsRadio && !bark.IsPreview && TerminatingOrDeleted(bark.Source!.Value)) // Arcane-Edit { _activeBarks.RemoveAt(i); continue; @@ -170,27 +187,37 @@ private void PlaySound(ActiveBark bark, char character) audioParams = audioParams.WithVolume(bark.Volume); var filter = Filter.Local(); - var soundEntity = bark.IsPreview + var soundEntity = bark.IsRadio || bark.IsPreview // Arcane-Edit ? _sharedAudio.PlayGlobal(sound, filter, false, audioParams) : _sharedAudio.PlayEntity(sound, filter, bark.Source!.Value, false, audioParams); - if (!bark.IsPreview && proto.Stop) + if (!bark.IsRadio && !bark.IsPreview && proto.Stop) // Arcane-Edit { if (_playingSounds.TryGetValue(GetNetEntity(bark.Source!.Value), out var playing)) _sharedAudio.Stop(playing); } - if (!bark.IsPreview && soundEntity is not null) + if (!bark.IsRadio && !bark.IsPreview && soundEntity is not null) // Arcane-Edit _playingSounds[GetNetEntity(bark.Source!.Value)] = soundEntity.Value.Entity; } - private float GetVolume(bool whisper, BarkPrototype proto) + private float GetVolume(bool whisper, BarkPrototype proto, bool radio = false) // Arcane-Edit { var volume = proto.Volume; if (whisper) volume = 0.05f + (volume - 0.05f) * 0.25f; + // Arcane-Start - Radio barks follow the Radio Volume setting, like the other radio sounds. + if (radio) + { + var radioVolume = _cfg.GetCVar(CCVars.RadioVolume) * ContentAudioSystem.RadioMultiplier; + volume *= radioVolume; + + return SharedAudioSystem.GainToVolume(volume); + } + // Arcane-End + var barksVolume = _cfg.GetCVar(GoobCVars.BarksVolume); volume *= barksVolume / 3f; @@ -201,6 +228,7 @@ private sealed class ActiveBark { public EntityUid? Source; public bool IsPreview; + public bool IsRadio; // Arcane public string Message = string.Empty; public BarkPrototype Prototype = default!; public float Volume; diff --git a/Content.Goobstation.Common/Barks/BarkEvents.cs b/Content.Goobstation.Common/Barks/BarkEvents.cs index f55618197e0..6e9d2546efe 100644 --- a/Content.Goobstation.Common/Barks/BarkEvents.cs +++ b/Content.Goobstation.Common/Barks/BarkEvents.cs @@ -3,11 +3,18 @@ namespace Content.Goobstation.Common.Barks; [Serializable, NetSerializable] -public sealed class PlayBarkEvent(NetEntity sourceUid, string message, bool whisper) : EntityEventArgs +public sealed class PlayBarkEvent(NetEntity sourceUid, string message, bool whisper, string? barkProtoId = null) : EntityEventArgs // Arcane { public NetEntity SourceUid { get; } = sourceUid; public string Message { get; } = message; public bool Whisper { get; } = whisper; + + // Arcane-Start + /// + /// Bark prototype id, when the bark is played for a radio listener that cannot see the speaker entity. + /// + public string? BarkProtoId { get; } = barkProtoId; + // Arcane-End } [Serializable, NetSerializable] diff --git a/Content.Server/Communications/CommunicationsConsoleSystem.cs b/Content.Server/Communications/CommunicationsConsoleSystem.cs index c69cce3958d..7c84e24b811 100644 --- a/Content.Server/Communications/CommunicationsConsoleSystem.cs +++ b/Content.Server/Communications/CommunicationsConsoleSystem.cs @@ -276,7 +276,8 @@ private void OnAnnounceMessage(EntityUid uid, CommunicationsConsoleComponent com if (TryComp(station, out var stationDataComp)) { var filter = _stationSystem.GetInStation(stationDataComp); - RaiseLocalEvent(new TTSAnnouncePlayEvent(msg, message.Actor, filter)); + var ttsEv = new TTSAnnouncePlayEvent(msg, message.Actor, filter); + RaiseLocalEvent(ref ttsEv); } // if (comp.AnnounceSentBy) diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 8e890bfd044..9eed678050e 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -10,6 +10,12 @@ using Robust.Shared.Network; using Robust.Shared.Player; using Content.Shared.Whitelist; +// Arcane-Start +using Content.Shared._Art.TTS; +using Content.Goobstation.Common.Barks; +using Content.Shared._Orion.Radio; +using Robust.Shared.Audio; +// Arcane-End namespace Content.Server.Radio.EntitySystems; @@ -57,8 +63,13 @@ private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpo && keys.Channels.Contains(args.Channel.ID) && _whitelist.IsWhitelistPassOrNull(args.Channel.SendWhitelist, uid)) // Goobstation - Whitelisted channels { - _radio.SendRadioMessage(uid, args.Message, args.Channel, component.Headset); - args.Channel = null; // prevent duplicate messages from other listeners. + // Arcane-Edit-Start + if (_radio.SendRadioMessage(uid, args.Message, args.Channel, component.Headset)) + { + args.RadioMessageSent = true; + args.Channel = null; // prevent duplicate messages from other listeners. + } + // Arcane-Edit-End } } @@ -104,6 +115,8 @@ public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = } } + private static readonly SoundSpecifier DefaultOnSound = new SoundPathSpecifier("/Audio/_Orion/Radio/basic.ogg"); // Arcane + private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref RadioReceiveEvent args) { // TODO: change this when a code refactor is done @@ -126,7 +139,42 @@ private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref Rad { Message = canUnderstand ? args.OriginalChatMsg : args.LanguageObfuscatedChatMsg }; + + // Arcane-Start + if (canUnderstand && args.Voice is { } voice) + { + var ev = new TTSRadioPlayEvent(args.OriginalChatMsg.Message, args.Language, voice); + RaiseLocalEvent(parent, ref ev); + } + // Arcane-End + _netMan.ServerSendMessage(msg, actor.PlayerSession.Channel); + + // Arcane-Start: Radio sound + var sound = args.Channel.OnSendSound ?? DefaultOnSound; + if (sound is SoundPathSpecifier sps) + { + RaiseNetworkEvent(new PlayRadioBarkEvent + { + Path = sps.Path.ToString(), + Params = sps.Params, + Source = GetNetEntity(args.MessageSource), + }, actor.PlayerSession.Channel); + } + else if (sound is SoundCollectionSpecifier) + { + Log.Warning($"Radio channel {args.Channel.ID} uses SoundCollectionSpecifier, which is not supported for PlayRadioBarkEvent. Falling back to silent playback."); + } + + if (parent != args.MessageSource + && TryComp(args.MessageSource, out var speech) + && speech.VoicePrototypeId is { } barkVoice) + { + RaiseNetworkEvent( + new PlayBarkEvent(GetNetEntity(args.MessageSource), args.OriginalChatMsg.Message, false, barkVoice), + actor.PlayerSession.Channel); + } + // Arcane-End } // Einstein Engines - Language end } diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index a2e7dc28dbb..24ac6fdcfea 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -25,6 +25,12 @@ using Content.Shared.Whitelist; // Goobstation using Content.Shared.StatusIcon; // Goobstation using Content.Goobstation.Shared.Radio; // Goobstation +// Arcane-Start +using Content.Shared._Art.TTS; // Arcane +using Content.Goobstation.Common.Barks; +using Content.Shared._Orion.Radio; +using Robust.Shared.Audio; +// Arcane-End namespace Content.Server.Radio.EntitySystems; @@ -64,8 +70,13 @@ private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent && component.Channels.Contains(args.Channel.ID) && _whitelist.IsWhitelistPassOrNull(args.Channel.SendWhitelist, uid)) // Goobstation - Whitelisted radio channels { - SendRadioMessage(uid, args.Message, args.Channel, uid, args.Language); // Einstein Engines - Language - args.Channel = null; // prevent duplicate messages from other listeners. + // Arcane-Edit-Start + if (SendRadioMessage(uid, args.Message, args.Channel, uid, args.Language)) // Einstein Engines - Language + { + args.RadioMessageSent = true; + args.Channel = null; // prevent duplicate messages from other listeners. + } + // Arcane-Edit-End } } @@ -76,11 +87,45 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c // Einstein Engines - Languages begin var listener = component.Owner; var msg = args.OriginalChatMsg; + var canUnderstand = listener == null || _language.CanUnderstand(listener, args.Language.ID); // Arcane - if (listener != null && !_language.CanUnderstand(listener, args.Language.ID)) + if (!canUnderstand) msg = args.LanguageObfuscatedChatMsg; + // Arcane-Start + if (canUnderstand && args.Voice is { } voice) + { + var ev = new TTSRadioPlayEvent(args.OriginalChatMsg.Message, args.Language, voice); + RaiseLocalEvent(uid, ref ev); + } + // Arcane-End _netMan.ServerSendMessage(new MsgChatMessage { Message = msg }, actor.PlayerSession.Channel); + + // Arcane-Start: Radio sound + var sound = args.Channel.OnSendSound ?? DefaultOnSound; + if (sound is SoundPathSpecifier sps) + { + RaiseNetworkEvent(new PlayRadioBarkEvent + { + Path = sps.Path.ToString(), + Params = sps.Params, + Source = GetNetEntity(args.MessageSource), + }, actor.PlayerSession.Channel); + } + else if (sound is SoundCollectionSpecifier) + { + Log.Warning($"Radio channel {args.Channel.ID} uses SoundCollectionSpecifier, which is not supported for PlayRadioBarkEvent. Falling back to silent playback."); + } + + if (uid != args.MessageSource + && TryComp(args.MessageSource, out var speech) + && speech.VoicePrototypeId is { } barkVoice) + { + RaiseNetworkEvent( + new PlayBarkEvent(GetNetEntity(args.MessageSource), args.OriginalChatMsg.Message, false, barkVoice), + actor.PlayerSession.Channel); + } + // Arcane-End // Einstein Engines - Languages end } } @@ -91,10 +136,12 @@ private void OnIntrinsicReceiveAttempt(EntityUid uid, IntrinsicRadioReceiverComp args.Cancelled = _whitelist.IsWhitelistFail(args.Channel.ReceiveWhitelist, uid); } + private static readonly SoundSpecifier DefaultOnSound = new SoundPathSpecifier("/Audio/_Orion/Radio/basic.ogg"); // Arcane + /// /// Send radio message to all active radio listeners /// - public void SendRadioMessage( + public bool SendRadioMessage( // Arcane-Edit EntityUid messageSource, string message, ProtoId channel, @@ -102,7 +149,7 @@ public void SendRadioMessage( LanguagePrototype? language = null, bool escapeMarkup = true) { - SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, language: language); // Einstein Engines - Language + return SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, language: language); // Einstein Engines - Language // Arcane-Edit } /// @@ -110,7 +157,8 @@ public void SendRadioMessage( /// /// Entity that spoke the message /// Entity that picked up the message and will send it, e.g. headset - public void SendRadioMessage( + /// Whether the message was transmitted to at least one radio listener. // Arcane-Edit + public bool SendRadioMessage( // Arcane-Edit EntityUid messageSource, string message, RadioChannelPrototype channel, @@ -123,12 +171,12 @@ public void SendRadioMessage( language = _language.GetLanguage(messageSource); if (!language.SpeechOverride.AllowRadio) - return; + return false; // Arcane-Edit // Einstein Engines - Language end // TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this. if (!_messages.Add(message)) - return; + return false; // Arcane-Edit var evt = new TransformSpeakerNameEvent(messageSource, MetaData(messageSource).EntityName); RaiseLocalEvent(messageSource, evt); @@ -185,7 +233,15 @@ public void SendRadioMessage( // Added GetNetEntity(messageSource), to source var obfuscatedWrapped = WrapRadioMessage(messageSource, channel, name, obfuscated, language, jobIcon, jobName); var notUdsMsg = new ChatMessage(ChatChannel.Radio, obfuscated, obfuscatedWrapped, GetNetEntity(messageSource), null); - var ev = new RadioReceiveEvent(messageSource, channel, msg, notUdsMsg, language, radioSource); + + // Arcane-Start + string? voice = null; + if (TryComp(messageSource, out var ttsComponent) + && ttsComponent.VoicePrototype is { } voiceId + && _prototype.TryIndex(voiceId, out var voicePrototype)) + voice = voicePrototype.Speaker; + // Arcane-End + var ev = new RadioReceiveEvent(messageSource, channel, msg, notUdsMsg, language, radioSource, voice); // Arcane-Edit // Einstein Engines - Language end var sendAttemptEv = new RadioSendAttemptEvent(channel, radioSource); @@ -198,6 +254,7 @@ public void SendRadioMessage( var sourceServerExempt = _exemptQuery.HasComp(radioSource); var radioQuery = EntityQueryEnumerator(); + var sent = false; // Arcane while (canSend && radioQuery.MoveNext(out var receiver, out var radio, out var transform)) { if (!radio.ReceiveAllChannels) @@ -225,6 +282,7 @@ public void SendRadioMessage( // send the message RaiseLocalEvent(receiver, ref ev); + sent = true; // Arcane } if (name != Name(messageSource)) @@ -234,6 +292,7 @@ public void SendRadioMessage( _replay.RecordServerMessage(msg); // Einstein Engines - Language _messages.Remove(message); + return sent; // Arcane } // Einstein Engines - Language begin diff --git a/Content.Server/Telephone/TelephoneSystem.cs b/Content.Server/Telephone/TelephoneSystem.cs index b1b723b1973..cab620107ba 100644 --- a/Content.Server/Telephone/TelephoneSystem.cs +++ b/Content.Server/Telephone/TelephoneSystem.cs @@ -26,6 +26,10 @@ using Robust.Shared.Replays; using Robust.Shared.Timing; using Robust.Shared.Utility; +// Arcane-Start +using Content.Shared.Holopad; +using Content.Shared._Art.TTS; +// Arcane-End namespace Content.Server.Telephone; @@ -120,7 +124,28 @@ private void OnTelephoneMessageReceived(Entity entity, ref T var range = args.TelephoneSource.Comp.LinkedTelephones.Count > 1 ? ChatTransmitRange.HideChat : ChatTransmitRange.GhostRangeLimit; var volume = entity.Comp.SpeakerVolume == TelephoneVolume.Speak ? InGameICChatType.Speak : InGameICChatType.Whisper; - _chat.TrySendInGameICMessage(speaker, args.Message, volume, range, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); // Eisntein Engines - Language + // Arcane-Start: Preserve the original speaker's TTS voice through the relay, so e.g. the station AI is heard + // at the receiving holopad with its own voice. The relay entity usually has no TTS component of its own. + // Holopad holograms apply a robotic effect to every relayed voice, like a holographic speaker. + if (TryComp(args.MessageSource, out var sourceTts) && sourceTts.VoicePrototype is { } voiceId) + { + var speakerTts = TryComp(speaker, out var existingTts) ? existingTts : AddComp(speaker); + + var oldVoice = speakerTts.VoicePrototype; + var oldEffect = speakerTts.Effect; + speakerTts.VoicePrototype = voiceId; + speakerTts.Effect = HasComp(speaker) ? "robotic" : sourceTts.Effect; + + _chat.TrySendInGameICMessage(speaker, args.Message, volume, range, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); + + speakerTts.VoicePrototype = oldVoice; + speakerTts.Effect = oldEffect; + if (existingTts == null) + RemComp(speaker); + } + else + // Arcane-End + _chat.TrySendInGameICMessage(speaker, args.Message, volume, range, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); // Eisntein Engines - Language } #endregion diff --git a/Content.Server/_Art/TTS/TTSManager.cs b/Content.Server/_Art/TTS/TTSManager.cs index 38b890cee96..049abeb96cc 100644 --- a/Content.Server/_Art/TTS/TTSManager.cs +++ b/Content.Server/_Art/TTS/TTSManager.cs @@ -17,7 +17,7 @@ namespace Content.Server._Art.TTS; /// /// TTS Manager for ntts.fdev.team API /// -public sealed class TTSManager +public sealed partial class TTSManager { private static readonly Histogram RequestTimings = Metrics.CreateHistogram( "tts_req_timings", @@ -36,7 +36,7 @@ public sealed class TTSManager "tts_reused_count", "Amount of reused TTS audio from cache."); - [Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!; + [Robust.Shared.IoC.Dependency] private IConfigurationManager _cfg = default!; private readonly HttpClient _httpClient = new(); diff --git a/Content.Server/_Art/TTS/TTSSystem.Cache.cs b/Content.Server/_Art/TTS/TTSSystem.Cache.cs index bb5210aa59f..e9dfc5412a9 100644 --- a/Content.Server/_Art/TTS/TTSSystem.Cache.cs +++ b/Content.Server/_Art/TTS/TTSSystem.Cache.cs @@ -8,7 +8,7 @@ namespace Content.Server._Art.TTS; // ReSharper disable once InconsistentNaming public sealed partial class TTSSystem { - [Dependency] private readonly IResourceManager _resourceManager = default!; + [Dependency] private IResourceManager _resourceManager = default!; private ResPath GetCacheId(TTSVoicePrototype voicePrototype, string cacheId) { diff --git a/Content.Server/_Art/TTS/TTSSystem.Preview.cs b/Content.Server/_Art/TTS/TTSSystem.Preview.cs index b5ef03da92a..fd400cc011e 100644 --- a/Content.Server/_Art/TTS/TTSSystem.Preview.cs +++ b/Content.Server/_Art/TTS/TTSSystem.Preview.cs @@ -7,7 +7,7 @@ namespace Content.Server._Art.TTS; // ReSharper disable once InconsistentNaming public sealed partial class TTSSystem { - [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private IRobustRandom _robustRandom = default!; private readonly List _sampleText = new() // TODO: Локализация? { diff --git a/Content.Server/_Art/TTS/TTSSystem.Sanitize.cs b/Content.Server/_Art/TTS/TTSSystem.Sanitize.cs index eb2fadb42ba..039f71fe7c9 100644 --- a/Content.Server/_Art/TTS/TTSSystem.Sanitize.cs +++ b/Content.Server/_Art/TTS/TTSSystem.Sanitize.cs @@ -165,6 +165,9 @@ private string ReplaceWord2Num(Match word) {"ви", "Вэ И"}, {"ии", "И И"}, {"осщ", "О Сэ Ща"}, + {"бдсм", "Бэ Дэ Эс Эм"}, + {"рп","Эр Пэ"}, + {"гк","Гэ Ка"} }; private static readonly IReadOnlyDictionary ReverseTranslit = diff --git a/Content.Server/_Art/TTS/TTSSystem.cs b/Content.Server/_Art/TTS/TTSSystem.cs index 40b4e7d2a6a..fd3b9fbfd3a 100644 --- a/Content.Server/_Art/TTS/TTSSystem.cs +++ b/Content.Server/_Art/TTS/TTSSystem.cs @@ -18,11 +18,11 @@ namespace Content.Server._Art.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 TTSManager _ttsManager = default!; - [Dependency] private readonly SharedTransformSystem _xforms = default!; - [Dependency] private readonly LanguageSystem _language = default!; + [Dependency] private IConfigurationManager _cfg = default!; + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private TTSManager _ttsManager = default!; + [Dependency] private SharedTransformSystem _xforms = default!; + [Dependency] private LanguageSystem _language = default!; private const int MaxMessageChars = 300; // Arcane private bool _isEnabled; @@ -47,7 +47,7 @@ private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySp if (!_isEnabled || args.Message.Length > MaxMessageChars) return; - if (args.Channel != null) + if (args.RadioMessageSent) return; if (!args.Language.SpeechOverride.RequireSpeech) @@ -68,7 +68,7 @@ private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySp HandleSay(uid, args.Message, args.Language, protoVoice.Speaker, effect); } - private void OnTTSRadioPlayEvent(EntityUid uid, ActorComponent comp, TTSRadioPlayEvent args) + private void OnTTSRadioPlayEvent(EntityUid uid, ActorComponent comp, ref TTSRadioPlayEvent args) { if (!_isEnabled || args.Message.Length > MaxMessageChars) return; @@ -77,7 +77,7 @@ private void OnTTSRadioPlayEvent(EntityUid uid, ActorComponent comp, TTSRadioPla } // Arcane-start - private void OnTTSAnnouncePlayEvent(TTSAnnouncePlayEvent args) + private void OnTTSAnnouncePlayEvent(ref TTSAnnouncePlayEvent args) { string? voice = null; if (TryComp(args.Sender, out var ttsComponent) @@ -88,7 +88,11 @@ private void OnTTSAnnouncePlayEvent(TTSAnnouncePlayEvent args) } if (voice != null) - Robust.Shared.Timing.Timer.Spawn(TimeSpan.FromSeconds(6), () => HandleReceiveRadio(args.Recievers, args.Message, voice, "announce")); + { + var receivers = args.Receievers; + var message = args.Message; + Robust.Shared.Timing.Timer.Spawn(TimeSpan.FromSeconds(6), () => HandleReceiveRadio(receivers, message, voice, "announce")); + } } private async void HandleReceiveRadio(Filter filter, string message, string speaker, string effect, LanguagePrototype? language = null) diff --git a/Content.Shared/Chat/SharedChatEvents.cs b/Content.Shared/Chat/SharedChatEvents.cs index 53d5d7e66b8..54a7b396f11 100644 --- a/Content.Shared/Chat/SharedChatEvents.cs +++ b/Content.Shared/Chat/SharedChatEvents.cs @@ -71,6 +71,14 @@ public sealed class EntitySpokeEvent : EntityEventArgs /// public RadioChannelPrototype? Channel; + // Arcane-Start + /// + /// Set to true when the message was actually sent over a radio channel. Used to suppress local TTS playback for + /// radio speech so it is only voiced for listeners on that frequency. + /// + public bool RadioMessageSent; + // Arcane-End + public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, bool isWhisper, LanguagePrototype language) // Einstein Engines - Language { Source = source; diff --git a/Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs b/Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs index ce6ccf83e5a..86d84f3a6b5 100644 --- a/Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs +++ b/Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs @@ -1,20 +1,10 @@ using Content.Shared._EinsteinEngines.Language; -using Content.Shared.Chat; using Robust.Shared.Player; namespace Content.Shared._Art.TTS; -public sealed class TTSRadioPlayEvent(Filter filter, string message, LanguagePrototype language, string voice) : EntityEventArgs -{ - public Filter Recievers { get; } = filter; - public string Message { get; } = message; - public LanguagePrototype Language { get; } = language; - public string Voice { get; } = voice; -} +[ByRefEvent] +public readonly record struct TTSRadioPlayEvent(string Message, LanguagePrototype Language, string Voice); -public sealed class TTSAnnouncePlayEvent(string message, EntityUid? sender, Filter filter) : EntityEventArgs -{ - public string Message { get; } = message; - public EntityUid? Sender { get; } = sender; - public Filter Recievers { get; } = filter; -} +[ByRefEvent] +public readonly record struct TTSAnnouncePlayEvent(string Message, EntityUid? Sender, Filter Receievers); diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index ac4013cbeab..b0be0a4a08f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -1067,3 +1067,7 @@ gender: male - type: Loadout prototypes: [ MobPollyGear ] + # Arcane-Start + - type: TTS + voice: Alchemist_dota_2 + # Arcane-End diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 03b2652459f..a38f17c8a6f 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -535,6 +535,10 @@ - NewKinPidgin - type: SpeechSynthesis # Corvax-Frontier-Barks voice: Borg + # Arcane-start + - type: TTS + voice: Glados + # Arcane-end # Hologram projection that the AI's eye tracks. - type: entity diff --git a/Resources/Prototypes/_EinsteinEngines/Entities/Mobs/Player/silicon_base.yml b/Resources/Prototypes/_EinsteinEngines/Entities/Mobs/Player/silicon_base.yml index 1f74248c0b1..d48b3143c4d 100644 --- a/Resources/Prototypes/_EinsteinEngines/Entities/Mobs/Player/silicon_base.yml +++ b/Resources/Prototypes/_EinsteinEngines/Entities/Mobs/Player/silicon_base.yml @@ -343,5 +343,6 @@ - type: ExaminableCharacter # WWDP # Arcane-start - type: DirectionalEmote + - type: TTS - type: ErpPanelOwner # Arcane-end