Реворк системы оповещений: пространственные динамики с очередью и умной фильтрацией чата (#3000)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: VigersRay <60344369+VigersRay@users.noreply.github.com> Co-authored-by: Vigers Ray <vigersray@gmail.com>
This commit is contained in:
parent
1bca528c17
commit
c39491fe62
15 changed files with 701 additions and 90 deletions
|
|
@ -31,7 +31,6 @@
|
|||
<CheckBox Name="TtsRadioGhostCheckBox" Text="{Loc 'ui-options-tts-radio-ghost-enabled'}" />
|
||||
<ui:OptionSlider Name="SliderTts" Title="{Loc 'ui-options-tts-volume'}" />
|
||||
<ui:OptionSlider Name="SliderTtsRadio" Title="{Loc 'ui-options-tts-radio-volume'}" />
|
||||
<ui:OptionSlider Name="SliderTtsAnnounce" Title="{Loc 'ui-options-tts-announce-volume'}" />
|
||||
<CheckBox Name="TapePlayerClientCheckBox" Text="{Loc 'ui-options-tape-player-enabled'}" />
|
||||
<CheckBox Name="JumpSoundDisableCheckBox" Text="{Loc 'ui-options-jump-sound-disable'}" />
|
||||
<CheckBox Name="VoteMusicDisableCheckBox" Text="{Loc 'ui-options-vote-music-disable'}" />
|
||||
|
|
|
|||
|
|
@ -32,11 +32,6 @@ public sealed partial class ExtraTab : Control
|
|||
SliderTtsRadio,
|
||||
scale: ContentAudioSystem.TtsMultiplier);
|
||||
|
||||
Control.AddOptionPercentSlider(
|
||||
SunriseCCVars.TTSAnnounceVolume,
|
||||
SliderTtsAnnounce,
|
||||
scale: ContentAudioSystem.TtsMultiplier);
|
||||
|
||||
Control.AddOptionCheckBox(SunriseCCVars.TTSClientEnabled, TtsClientCheckBox);
|
||||
Control.AddOptionCheckBox(SunriseCCVars.TTSClientQueueEnabled, TtsClientCheckBoxQueue);
|
||||
Control.AddOptionCheckBox(SunriseCCVars.TTSRadioGhostEnabled, TtsRadioGhostCheckBox);
|
||||
|
|
|
|||
|
|
@ -36,25 +36,22 @@ public sealed class TTSSystem : EntitySystem
|
|||
private float _volume;
|
||||
private float _radioVolume;
|
||||
private int _fileIdx;
|
||||
private float _volumeAnnounce;
|
||||
private bool _isQueueEnabled;
|
||||
private bool _ghostRadioEnabled;
|
||||
private readonly Queue<QueuedTts> _ttsQueue = new();
|
||||
private (EntityUid Entity, AudioComponent Component)? _currentPlaying;
|
||||
private static readonly AudioResource EmptyAudioResource = new();
|
||||
|
||||
public sealed class QueuedTts(byte[] data, TtsType ttsType, ResolvedSoundSpecifier? announcementSound = null)
|
||||
public sealed class QueuedTts(byte[] data, TtsType ttsType)
|
||||
{
|
||||
public byte[] Data = data;
|
||||
public ResolvedSoundSpecifier? AnnouncementSound = announcementSound;
|
||||
public TtsType TtsType = ttsType;
|
||||
}
|
||||
|
||||
public enum TtsType
|
||||
{
|
||||
Voice,
|
||||
Radio,
|
||||
Announce
|
||||
Radio
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
|
|
@ -63,12 +60,10 @@ public sealed class TTSSystem : EntitySystem
|
|||
_res.AddRoot(Prefix, ContentRoot);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSVolume, OnTtsVolumeChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSRadioVolume, OnTtsRadioVolumeChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSAnnounceVolume, OnTtsAnnounceVolumeChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSClientEnabled, OnTtsClientOptionChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSClientQueueEnabled, OnTTSQueueOptionChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSRadioGhostEnabled, OnTtsRadioGhostChanged, true);
|
||||
SubscribeNetworkEvent<PlayTTSEvent>(OnPlayTTS);
|
||||
SubscribeNetworkEvent<AnnounceTtsEvent>(OnAnnounceTTSPlay);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
|
@ -76,7 +71,6 @@ public sealed class TTSSystem : EntitySystem
|
|||
base.Shutdown();
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.TTSVolume, OnTtsVolumeChanged);
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.TTSRadioVolume, OnTtsRadioVolumeChanged);
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.TTSAnnounceVolume, OnTtsAnnounceVolumeChanged);
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.TTSClientEnabled, OnTtsClientOptionChanged);
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.TTSClientQueueEnabled, OnTTSQueueOptionChanged);
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.TTSRadioGhostEnabled, OnTtsRadioGhostChanged);
|
||||
|
|
@ -105,10 +99,6 @@ public sealed class TTSSystem : EntitySystem
|
|||
{
|
||||
_isQueueEnabled = option;
|
||||
}
|
||||
private void OnTtsAnnounceVolumeChanged(float volume)
|
||||
{
|
||||
_volumeAnnounce = volume;
|
||||
}
|
||||
|
||||
private void OnTtsClientOptionChanged(bool option)
|
||||
{
|
||||
|
|
@ -120,15 +110,7 @@ public sealed class TTSSystem : EntitySystem
|
|||
_ghostRadioEnabled = option;
|
||||
}
|
||||
|
||||
private void OnAnnounceTTSPlay(AnnounceTtsEvent ev)
|
||||
{
|
||||
if (_volumeAnnounce == 0)
|
||||
return;
|
||||
|
||||
var entry = new QueuedTts(ev.Data, TtsType.Announce, ev.AnnouncementSound);
|
||||
|
||||
_ttsQueue.Enqueue(entry);
|
||||
}
|
||||
|
||||
private void PlayNextInQueue()
|
||||
{
|
||||
|
|
@ -145,9 +127,6 @@ public sealed class TTSSystem : EntitySystem
|
|||
case TtsType.Radio:
|
||||
volume = _radioVolume;
|
||||
break;
|
||||
case TtsType.Announce:
|
||||
volume = _volumeAnnounce;
|
||||
break;
|
||||
case TtsType.Voice:
|
||||
volume = _volume;
|
||||
break;
|
||||
|
|
@ -155,10 +134,6 @@ public sealed class TTSSystem : EntitySystem
|
|||
|
||||
var finalParams = AudioParams.Default.WithVolume(SharedAudioSystem.GainToVolume(volume));
|
||||
|
||||
if (entry.AnnouncementSound != null)
|
||||
{
|
||||
_currentPlaying = _audio.PlayGlobal(entry.AnnouncementSound, new EntityUid(), finalParams.AddVolume(-5f));
|
||||
}
|
||||
_currentPlaying = PlayTTSBytes(entry.Data, null, finalParams, true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Content.Server._Sunrise.Chat;
|
||||
using Content.Server._Sunrise.Chat.Sanitization;
|
||||
using Content.Server._Sunrise.AnnouncementSpeaker;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
|
|
@ -66,6 +67,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly AnnouncementSpeakerSystem _announcementSpeaker = default!;
|
||||
|
||||
public const string DefaultAnnouncementSound = "/Audio/_Sunrise/Announcements/announce_dig.ogg"; // Sunrise-edit
|
||||
|
||||
|
|
@ -350,7 +352,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
#region Announcements
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement to all.
|
||||
/// Dispatches an announcement to all stations through their speaker networks.
|
||||
/// </summary>
|
||||
/// <param name="message">The contents of the message</param>
|
||||
/// <param name="sender">The sender (Communications Console in Communications Console Announcement)</param>
|
||||
|
|
@ -370,19 +372,25 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
sender ??= Loc.GetString("chat-manager-sender-announcement");
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
|
||||
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
|
||||
|
||||
// Sunrise-start
|
||||
if (playDefault && announcementSound == null)
|
||||
|
||||
// Sunrise-start - Only show in chat for players with working speakers nearby
|
||||
var filteredPlayers = GetPlayersWithWorkingSpeakers();
|
||||
if (filteredPlayers.Recipients.Any())
|
||||
{
|
||||
announcementSound ??= new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
_chatManager.ChatMessageToManyFiltered(filteredPlayers, ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
|
||||
}
|
||||
// Sunrise-end
|
||||
|
||||
if (playTts && announcementSound != null)
|
||||
// Sunrise-start - Use speaker network instead of global broadcast
|
||||
if (playTts && (playDefault || announcementSound != null))
|
||||
{
|
||||
//_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
|
||||
var announcementEv = new AnnouncementSpokeEvent(Filter.Broadcast(), message, _audio.ResolveSound(announcementSound), announceVoice);
|
||||
RaiseLocalEvent(announcementEv);
|
||||
if (playDefault && announcementSound == null)
|
||||
{
|
||||
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
}
|
||||
|
||||
// Send announcement to all stations through their speaker networks
|
||||
_announcementSpeaker.DispatchAnnouncementToAllStations(message, announcementSound, announceVoice);
|
||||
}
|
||||
// Sunrise-end
|
||||
|
||||
|
|
@ -413,17 +421,47 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
sender ??= Loc.GetString("chat-manager-sender-announcement");
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
|
||||
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
|
||||
// Sunrise-start
|
||||
if (playDefault && announcementSound == null)
|
||||
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
|
||||
if (playTts && announcementSound != null)
|
||||
|
||||
// Sunrise-start - Filter chat recipients by working speakers
|
||||
var filteredChatPlayers = FilterPlayersByWorkingSpeakers(filter);
|
||||
if (filteredChatPlayers.Recipients.Any())
|
||||
{
|
||||
//_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
|
||||
RaiseLocalEvent(new AnnouncementSpokeEvent(filter, message, _audio.ResolveSound(announcementSound), announceVoice));
|
||||
_chatManager.ChatMessageToManyFiltered(filteredChatPlayers, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
|
||||
}
|
||||
// Sunrise-edit
|
||||
// Sunrise-end
|
||||
|
||||
// Sunrise-start - For filtered announcements, we may want to try speaker network if source is on a station
|
||||
if (playTts && (playDefault || announcementSound != null))
|
||||
{
|
||||
if (playDefault && announcementSound == null)
|
||||
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
|
||||
var resolvedSound = announcementSound != null ? _audio.ResolveSound(announcementSound) : null;
|
||||
|
||||
// If we have a source, try to use the station's speaker network
|
||||
if (source != null)
|
||||
{
|
||||
var station = _stationSystem.GetOwningStation(source.Value);
|
||||
if (station != null)
|
||||
{
|
||||
_announcementSpeaker.DispatchAnnouncementToSpeakers(station.Value, message, announcementSound, announceVoice);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to old broadcast system for non-station sources
|
||||
var announcementEv = new AnnouncementSpokeEvent(filter, message, resolvedSound, announceVoice);
|
||||
RaiseLocalEvent(announcementEv);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No source, fallback to old broadcast system
|
||||
var announcementEv = new AnnouncementSpokeEvent(filter, message, resolvedSound, announceVoice);
|
||||
RaiseLocalEvent(announcementEv);
|
||||
}
|
||||
}
|
||||
// Sunrise-end
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}");
|
||||
}
|
||||
|
||||
|
|
@ -462,18 +500,24 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
|
||||
var filter = _stationSystem.GetInStation(stationDataComp);
|
||||
|
||||
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride);
|
||||
|
||||
// Sunrise-start
|
||||
if (playDefault && announcementSound == null)
|
||||
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
|
||||
if (playTts && announcementSound != null)
|
||||
// Sunrise-start - Filter chat recipients by working speakers
|
||||
var filteredChatPlayers = FilterPlayersByWorkingSpeakers(filter);
|
||||
if (filteredChatPlayers.Recipients.Any())
|
||||
{
|
||||
//_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
|
||||
RaiseLocalEvent(new AnnouncementSpokeEvent(filter, message, _audio.ResolveSound(announcementSound), announceVoice));
|
||||
_chatManager.ChatMessageToManyFiltered(filteredChatPlayers, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride);
|
||||
}
|
||||
// Sunrise-edit
|
||||
// Sunrise-end
|
||||
|
||||
// Sunrise-start - Use speaker network for station announcements
|
||||
if (playTts && (playDefault || announcementSound != null))
|
||||
{
|
||||
if (playDefault && announcementSound == null)
|
||||
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
|
||||
// Send announcement to this specific station's speaker network
|
||||
_announcementSpeaker.DispatchAnnouncementToSpeakers(station.Value, message, announcementSound, announceVoice);
|
||||
}
|
||||
// Sunrise-end
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
|
||||
}
|
||||
|
|
@ -823,6 +867,53 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
|
||||
#region Utility
|
||||
|
||||
/// <summary>
|
||||
/// Gets all players who have working announcement speakers nearby.
|
||||
/// Used to filter chat recipients for announcements.
|
||||
/// </summary>
|
||||
private Filter GetPlayersWithWorkingSpeakers()
|
||||
{
|
||||
var filteredPlayers = Filter.Empty();
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (player.AttachedEntity is not { Valid: true } playerEntity)
|
||||
continue;
|
||||
|
||||
if (_announcementSpeaker.HasWorkingSpeakersNearby(playerEntity))
|
||||
{
|
||||
filteredPlayers = filteredPlayers.AddPlayer(player);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPlayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters an existing filter to only include players with working speakers nearby.
|
||||
/// </summary>
|
||||
private Filter FilterPlayersByWorkingSpeakers(Filter originalFilter)
|
||||
{
|
||||
var filteredPlayers = Filter.Empty();
|
||||
|
||||
foreach (var player in originalFilter.Recipients)
|
||||
{
|
||||
if (player.AttachedEntity is not { Valid: true } playerEntity)
|
||||
continue;
|
||||
|
||||
if (_announcementSpeaker.HasWorkingSpeakersNearby(playerEntity))
|
||||
{
|
||||
filteredPlayers = filteredPlayers.AddPlayer(player);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPlayers;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility
|
||||
|
||||
private enum MessageRangeCheckResult
|
||||
{
|
||||
Disallowed,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ namespace Content.Server.Communications
|
|||
/// In practise this removes the "Sent by ScugMcWawa (Slugcat Captain)" at the bottom of the announcement.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool AnnounceSentBy = true;
|
||||
public bool AnnounceSentBy = false;
|
||||
|
||||
// Sunrise-Start
|
||||
[DataField("announceVoice", customTypeSerializer:typeof(PrototypeIdSerializer<TTSVoicePrototype>))]
|
||||
|
|
|
|||
|
|
@ -262,23 +262,23 @@ namespace Content.Server.Communications
|
|||
|
||||
if (comp.AnnounceSentBy)
|
||||
msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author;
|
||||
// Sunrise-start
|
||||
var voice = comp.AnnounceVoice;
|
||||
if (TryComp<TTSComponent>(message.Actor, out var ttsComponent))
|
||||
{
|
||||
voice = ttsComponent.VoicePrototypeId;
|
||||
}
|
||||
// Sunrise-end
|
||||
|
||||
if (comp.Global)
|
||||
{
|
||||
// Sunrise-start
|
||||
var voice = comp.AnnounceVoice;
|
||||
if (TryComp<TTSComponent>(message.Actor, out var ttsComponent))
|
||||
{
|
||||
voice = ttsComponent.VoicePrototypeId;
|
||||
}
|
||||
// Sunrise-end
|
||||
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color, announceVoice: voice); // Sunrise-edit
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following global announcement: {msg}");
|
||||
return;
|
||||
}
|
||||
|
||||
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color, announceVoice: comp.AnnounceVoice);
|
||||
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color, announceVoice: voice);
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following station announcement: {msg}");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,415 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server._Sunrise.TTS;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared._Sunrise.AnnouncementSpeaker.Components;
|
||||
using Content.Shared._Sunrise.AnnouncementSpeaker.Events;
|
||||
using Content.Shared._Sunrise.TTS;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared._Sunrise.SunriseCCVars;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server._Sunrise.AnnouncementSpeaker;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a queued announcement waiting to be played.
|
||||
/// </summary>
|
||||
public sealed class QueuedAnnouncement
|
||||
{
|
||||
public EntityUid Station { get; set; }
|
||||
public string Message { get; set; } = "";
|
||||
public ResolvedSoundSpecifier? AnnouncementSound { get; set; }
|
||||
public string? AnnounceVoice { get; set; }
|
||||
public byte[]? TtsData { get; set; }
|
||||
public TimeSpan QueuedAt { get; set; }
|
||||
|
||||
public QueuedAnnouncement(EntityUid station, string message, ResolvedSoundSpecifier? announcementSound, string? announceVoice, byte[]? ttsData)
|
||||
{
|
||||
Station = station;
|
||||
Message = message;
|
||||
AnnouncementSound = announcementSound;
|
||||
AnnounceVoice = announceVoice;
|
||||
TtsData = ttsData;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System that manages announcement speakers distributed across stations.
|
||||
/// Replaces global announcements with spatial audio from speaker networks.
|
||||
/// </summary>
|
||||
public sealed class AnnouncementSpeakerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private bool _isEnabled;
|
||||
private string _defaultAnnounceVoice = "Hanson";
|
||||
|
||||
// Queue system for preventing overlapping announcements
|
||||
private readonly Queue<QueuedAnnouncement> _announcementQueue = new();
|
||||
private bool _isPlayingAnnouncement = false;
|
||||
private TimeSpan _currentAnnouncementEndTime;
|
||||
private const float AnnouncementDurationEstimate = 5.0f; // Estimate 5 seconds per announcement
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSEnabled, v => _isEnabled = v, true);
|
||||
SubscribeLocalEvent<AnnouncementSpeakerEvent>(OnAnnouncementSpeaker);
|
||||
// Note: SpeakerPlayAnnouncementEvent is handled by TTSSystem for the component
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
ProcessAnnouncementQueue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the announcement queue and plays the next announcement if none is currently playing.
|
||||
/// </summary>
|
||||
private void ProcessAnnouncementQueue()
|
||||
{
|
||||
// Check if current announcement has finished
|
||||
if (_isPlayingAnnouncement && _timing.CurTime >= _currentAnnouncementEndTime)
|
||||
{
|
||||
_isPlayingAnnouncement = false;
|
||||
}
|
||||
|
||||
// If not playing and have queued announcements, play next one
|
||||
if (!_isPlayingAnnouncement && _announcementQueue.Count > 0)
|
||||
{
|
||||
var announcement = _announcementQueue.Dequeue();
|
||||
PlayAnnouncementNow(announcement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately plays an announcement without queuing.
|
||||
/// </summary>
|
||||
private void PlayAnnouncementNow(QueuedAnnouncement announcement)
|
||||
{
|
||||
var duration = TimeSpan.FromSeconds(AnnouncementDurationEstimate);
|
||||
if (announcement.TtsData != null)
|
||||
duration = GetAudioDurationFromBytes(announcement.TtsData);
|
||||
_isPlayingAnnouncement = true;
|
||||
_currentAnnouncementEndTime = _timing.CurTime + duration;
|
||||
|
||||
var ev = new AnnouncementSpeakerEvent(
|
||||
announcement.Station,
|
||||
announcement.Message,
|
||||
announcement.AnnouncementSound,
|
||||
announcement.AnnounceVoice,
|
||||
announcement.TtsData
|
||||
);
|
||||
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles station-wide announcements by finding all speakers on the station and playing the announcement through them.
|
||||
/// </summary>
|
||||
private void OnAnnouncementSpeaker(ref AnnouncementSpeakerEvent ev)
|
||||
{
|
||||
// Find all speakers on the station
|
||||
var speakers = GetStationSpeakers(ev.Station);
|
||||
|
||||
if (speakers.Count == 0)
|
||||
{
|
||||
// Fallback: If no speakers are found, log a warning
|
||||
// In the future, this could send to a single communications console or similar
|
||||
Logger.Warning($"No announcement speakers found on station {ToPrettyString(ev.Station)}. Announcement not played: {ev.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Play announcement sound via PVS for each speaker on server side
|
||||
if (ev.AnnouncementSound != null)
|
||||
{
|
||||
foreach (var speaker in speakers)
|
||||
{
|
||||
if (!TryComp<AnnouncementSpeakerComponent>(speaker, out var speakerComp))
|
||||
continue;
|
||||
|
||||
// Check if speaker is enabled and has power
|
||||
if (!speakerComp.Enabled)
|
||||
continue;
|
||||
|
||||
if (speakerComp.RequiresPower)
|
||||
{
|
||||
if (!TryComp<ApcPowerReceiverComponent>(speaker, out var powerReceiver) || !powerReceiver.Powered)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Play announcement sound via PVS from this speaker
|
||||
var audioParams = AudioParams.Default.WithVolume(-2f * speakerComp.VolumeModifier).WithMaxDistance(speakerComp.Range);
|
||||
_audioSystem.PlayPvs(ev.AnnouncementSound, speaker, audioParams);
|
||||
}
|
||||
}
|
||||
|
||||
// Передаём TTS сразу всем динамикам
|
||||
var speakerEvent = new SpeakerPlayAnnouncementEvent(ev.Message, ev.AnnouncementSound, ev.AnnounceVoice, ev.TtsData);
|
||||
foreach (var speaker in speakers)
|
||||
{
|
||||
RaiseLocalEvent(speaker, ref speakerEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all functional announcement speakers on a station.
|
||||
/// </summary>
|
||||
private List<EntityUid> GetStationSpeakers(EntityUid station)
|
||||
{
|
||||
var speakers = new List<EntityUid>();
|
||||
|
||||
if (!TryComp<StationDataComponent>(station, out var stationData))
|
||||
return speakers;
|
||||
|
||||
// Look through all grids on the station for speakers
|
||||
foreach (var grid in stationData.Grids)
|
||||
{
|
||||
var query = EntityQueryEnumerator<AnnouncementSpeakerComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var speakerComp, out var xform))
|
||||
{
|
||||
// Check if the speaker is on this grid
|
||||
if (xform.GridUid == grid)
|
||||
{
|
||||
speakers.Add(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return speakers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement to all speakers on a station.
|
||||
/// This is the main entry point for the announcement speaker system.
|
||||
/// Uses the queue system to prevent overlapping announcements.
|
||||
/// </summary>
|
||||
public async void DispatchAnnouncementToSpeakers(EntityUid station, string message, SoundSpecifier? announcementSound = null, string? announceVoice = null)
|
||||
{
|
||||
var resolvedSound = announcementSound != null ? _audioSystem.ResolveSound(announcementSound) : null;
|
||||
if (!_isEnabled)
|
||||
return;
|
||||
if (!GetVoicePrototype(announceVoice ?? _defaultAnnounceVoice, out var protoVoice))
|
||||
return;
|
||||
var generatedTts = await GenerateTtsForAnnouncement(message, protoVoice);
|
||||
|
||||
var queuedAnnouncement = new QueuedAnnouncement(station, message, resolvedSound, announceVoice, generatedTts)
|
||||
{
|
||||
QueuedAt = _timing.CurTime
|
||||
};
|
||||
|
||||
if (!_isPlayingAnnouncement)
|
||||
{
|
||||
// If no announcement is playing, play immediately
|
||||
PlayAnnouncementNow(queuedAnnouncement);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Queue the announcement to play after current one finishes
|
||||
_announcementQueue.Enqueue(queuedAnnouncement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement to speakers on all stations.
|
||||
/// Used for server-wide announcements like round start/end.
|
||||
/// Uses the queue system to prevent overlapping announcements.
|
||||
/// </summary>
|
||||
public async void DispatchAnnouncementToAllStations(string message, SoundSpecifier? announcementSound = null, string? announceVoice = null)
|
||||
{
|
||||
var resolvedSound = announcementSound != null ? _audioSystem.ResolveSound(announcementSound) : null;
|
||||
if (!_isEnabled)
|
||||
return;
|
||||
if (!GetVoicePrototype(announceVoice ?? _defaultAnnounceVoice, out var protoVoice))
|
||||
return;
|
||||
var generatedTts = await GenerateTtsForAnnouncement(message, protoVoice);
|
||||
|
||||
var stationQuery = EntityQueryEnumerator<StationDataComponent>();
|
||||
while (stationQuery.MoveNext(out var stationUid, out var stationData))
|
||||
{
|
||||
var queuedAnnouncement = new QueuedAnnouncement(stationUid, message, resolvedSound, announceVoice, generatedTts)
|
||||
{
|
||||
QueuedAt = _timing.CurTime
|
||||
};
|
||||
|
||||
if (!_isPlayingAnnouncement)
|
||||
{
|
||||
// If no announcement is playing, play immediately
|
||||
PlayAnnouncementNow(queuedAnnouncement);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Queue the announcement to play after current one finishes
|
||||
_announcementQueue.Enqueue(queuedAnnouncement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a voice prototype by ID, with fallback to default voice.
|
||||
/// </summary>
|
||||
private bool GetVoicePrototype(string voiceId, [NotNullWhen(true)] out TTSVoicePrototype? voicePrototype)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(voiceId, out voicePrototype))
|
||||
{
|
||||
return _prototypeManager.TryIndex("father_grigori", out voicePrototype);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates TTS audio for an announcement with the megaphone effect.
|
||||
/// </summary>
|
||||
private async Task<byte[]?> GenerateTtsForAnnouncement(string text, TTSVoicePrototype voicePrototype)
|
||||
{
|
||||
try
|
||||
{
|
||||
var textSanitized = Sanitize(text);
|
||||
if (textSanitized == "") return null;
|
||||
if (char.IsLetter(textSanitized[^1]))
|
||||
textSanitized += ".";
|
||||
|
||||
// Use TTS manager directly to generate with megaphone effect
|
||||
var ttsManager = IoCManager.Resolve<TTSManager>();
|
||||
return await ttsManager.ConvertTextToSpeechAnnounce(voicePrototype, textSanitized);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"TTS System error in announcement generation: {e.Message}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes text for TTS generation.
|
||||
/// </summary>
|
||||
private string Sanitize(string text)
|
||||
{
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a player has any working announcement speakers within range.
|
||||
/// Used to determine if they should receive announcement messages in chat.
|
||||
/// </summary>
|
||||
public bool HasWorkingSpeakersNearby(EntityUid playerEntity)
|
||||
{
|
||||
if (!TryComp<TransformComponent>(playerEntity, out var playerTransform))
|
||||
return false;
|
||||
|
||||
var playerPos = playerTransform.Coordinates;
|
||||
|
||||
// Find all speakers and check if any are in range and working
|
||||
var query = EntityQueryEnumerator<AnnouncementSpeakerComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var speakerUid, out var speakerComp, out var speakerTransform))
|
||||
{
|
||||
// Check if speaker is enabled
|
||||
if (!speakerComp.Enabled)
|
||||
continue;
|
||||
|
||||
// Check if speaker has power (if required)
|
||||
if (speakerComp.RequiresPower)
|
||||
{
|
||||
if (!TryComp<ApcPowerReceiverComponent>(speakerUid, out var powerReceiver) || !powerReceiver.Powered)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if player is within range of this speaker
|
||||
if (playerPos.TryDistance(EntityManager, speakerTransform.Coordinates, out var distance) &&
|
||||
distance <= speakerComp.Range)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all speakers on all stations that are working.
|
||||
/// Used to determine if any announcements can be played at all.
|
||||
/// </summary>
|
||||
public bool HasAnyWorkingSpeakers()
|
||||
{
|
||||
var query = EntityQueryEnumerator<AnnouncementSpeakerComponent>();
|
||||
while (query.MoveNext(out var speakerUid, out var speakerComp))
|
||||
{
|
||||
// Check if speaker is enabled
|
||||
if (!speakerComp.Enabled)
|
||||
continue;
|
||||
|
||||
// Check if speaker has power (if required)
|
||||
if (speakerComp.RequiresPower)
|
||||
{
|
||||
if (!TryComp<ApcPowerReceiverComponent>(speakerUid, out var powerReceiver) || !powerReceiver.Powered)
|
||||
continue;
|
||||
}
|
||||
|
||||
return true; // Found at least one working speaker
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static TimeSpan GetWavDurationFromBytes(byte[] wavData)
|
||||
{
|
||||
// WAV header is 44 bytes for PCM
|
||||
if (wavData.Length < 44)
|
||||
return TimeSpan.Zero;
|
||||
int sampleRate = BitConverter.ToInt32(wavData, 24);
|
||||
short channels = BitConverter.ToInt16(wavData, 22);
|
||||
short bitsPerSample = BitConverter.ToInt16(wavData, 34);
|
||||
int dataSize = BitConverter.ToInt32(wavData, 40);
|
||||
int bytesPerSample = bitsPerSample / 8;
|
||||
int totalSamples = dataSize / (bytesPerSample * channels);
|
||||
if (sampleRate <= 0 || channels <= 0 || bitsPerSample <= 0)
|
||||
return TimeSpan.Zero;
|
||||
double durationSeconds = (double)totalSamples / sampleRate;
|
||||
return TimeSpan.FromSeconds(durationSeconds);
|
||||
}
|
||||
|
||||
private static TimeSpan GetAudioDurationFromBytes(byte[] audioData, string? fileType = null)
|
||||
{
|
||||
if (fileType == "wav" || (audioData.Length > 12 && audioData[0] == 'R' && audioData[1] == 'I' && audioData[2] == 'F' && audioData[8] == 'W' && audioData[9] == 'A' && audioData[10] == 'V'))
|
||||
{
|
||||
return GetWavDurationFromBytes(audioData);
|
||||
}
|
||||
if (fileType == "ogg" || (audioData.Length > 4 && audioData[0] == 'O' && audioData[1] == 'g' && audioData[2] == 'g' && audioData[3] == 'S'))
|
||||
{
|
||||
try
|
||||
{
|
||||
var nvorbisType = Type.GetType("NVorbis.VorbisReader, NVorbis");
|
||||
if (nvorbisType != null)
|
||||
{
|
||||
using var stream = new MemoryStream(audioData);
|
||||
using var reader = (IDisposable)Activator.CreateInstance(nvorbisType, stream, false)!;
|
||||
var totalTimeProp = nvorbisType.GetProperty("TotalTime");
|
||||
if (totalTimeProp != null)
|
||||
{
|
||||
var totalTime = totalTimeProp.GetValue(reader);
|
||||
if (totalTime is TimeSpan ts)
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// NVorbis не доступен или ошибка — fallback
|
||||
}
|
||||
double fallbackBitrate = 128000.0; // 128 кбит/с
|
||||
double duration = audioData.Length * 8.0 / fallbackBitrate;
|
||||
return TimeSpan.FromSeconds(duration);
|
||||
}
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
|
@ -42,12 +42,16 @@ public sealed class TTSManager
|
|||
|
||||
private ISawmill _sawmill = default!;
|
||||
private string _apiUrl = string.Empty;
|
||||
private string _radioEffect = string.Empty;
|
||||
private string _announceEffect = string.Empty;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = Logger.GetSawmill("tts");
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSApiUrl, OnApiUrlChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSApiToken, OnApiTokenChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSRadioEffect, OnRadioEffectChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.TTSAnnounceEffect, OnAnnounceEffectChanged, true);
|
||||
}
|
||||
|
||||
private void OnApiUrlChanged(string value)
|
||||
|
|
@ -60,6 +64,16 @@ public sealed class TTSManager
|
|||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", value);
|
||||
}
|
||||
|
||||
private void OnRadioEffectChanged(string value)
|
||||
{
|
||||
_radioEffect = value;
|
||||
}
|
||||
|
||||
private void OnAnnounceEffectChanged(string value)
|
||||
{
|
||||
_announceEffect = value;
|
||||
}
|
||||
|
||||
public async Task<byte[]?> ConvertTextToSpeech(TTSVoicePrototype voicePrototype, string text, string? effect = null)
|
||||
{
|
||||
WantedCount.Inc();
|
||||
|
|
@ -137,16 +151,14 @@ public sealed class TTSManager
|
|||
public async Task<byte[]?> ConvertTextToSpeechRadio(TTSVoicePrototype voicePrototype, string text)
|
||||
{
|
||||
WantedRadioCount.Inc();
|
||||
var soundData = await ConvertTextToSpeech(voicePrototype, text, "radio");
|
||||
|
||||
var soundData = await ConvertTextToSpeech(voicePrototype, text, _radioEffect);
|
||||
return soundData;
|
||||
}
|
||||
|
||||
public async Task<byte[]?> ConvertTextToSpeechAnnounce(TTSVoicePrototype voicePrototype, string text)
|
||||
{
|
||||
WantedAnnounceCount.Inc();
|
||||
var soundData = await ConvertTextToSpeech(voicePrototype, text, "announce");
|
||||
|
||||
var soundData = await ConvertTextToSpeech(voicePrototype, text, _announceEffect);
|
||||
return soundData;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,13 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared._Sunrise.SunriseCCVars;
|
||||
using Content.Shared._Sunrise.TTS;
|
||||
using Content.Shared._Sunrise.AnnouncementSpeaker.Components;
|
||||
using Content.Shared._Sunrise.AnnouncementSpeaker.Events;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Player;
|
||||
|
|
@ -58,6 +63,7 @@ public sealed partial class TTSSystem : EntitySystem
|
|||
SubscribeLocalEvent<TTSComponent, EntitySpokeEvent>(OnEntitySpoke);
|
||||
SubscribeLocalEvent<RadioSpokeEvent>(OnRadioReceiveEvent);
|
||||
SubscribeLocalEvent<AnnouncementSpokeEvent>(OnAnnouncementSpoke);
|
||||
SubscribeLocalEvent<AnnouncementSpeakerComponent, SpeakerPlayAnnouncementEvent>(OnSpeakerPlayAnnouncement);
|
||||
|
||||
SubscribeNetworkEvent<RequestPreviewTTSEvent>(OnRequestPreviewTTS);
|
||||
SubscribeNetworkEvent<ClientOptionTTSEvent>(OnClientOptionTTS);
|
||||
|
|
@ -132,6 +138,12 @@ public sealed partial class TTSSystem : EntitySystem
|
|||
return;
|
||||
}
|
||||
|
||||
// Play announcement sound first if available (for global announcements)
|
||||
if (args.AnnouncementSound != null)
|
||||
{
|
||||
_audioSystem.PlayGlobal(args.AnnouncementSound, args.Source, true);
|
||||
}
|
||||
|
||||
if (!_isEnabled ||
|
||||
args.Message.Length > MaxMessageChars * 2 ||
|
||||
!GetVoicePrototype(args.AnnounceVoice ?? _defaultAnnounceVoice, out var protoVoice))
|
||||
|
|
@ -139,7 +151,55 @@ public sealed partial class TTSSystem : EntitySystem
|
|||
|
||||
var soundData = await GenerateTTS(args.Message, protoVoice, isAnnounce: true);
|
||||
soundData ??= [];
|
||||
RaiseNetworkEvent(new AnnounceTtsEvent(soundData, args.AnnouncementSound), args.Source.RemovePlayers(_ignoredRecipients));
|
||||
RaiseNetworkEvent(new PlayTTSEvent(soundData, null, isRadio: false), args.Source.RemovePlayers(_ignoredRecipients));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles TTS generation for speaker-based announcements.
|
||||
/// This is the new system that replaces global broadcast announcements.
|
||||
/// </summary>
|
||||
private void OnSpeakerPlayAnnouncement(EntityUid speakerUid, AnnouncementSpeakerComponent component, ref SpeakerPlayAnnouncementEvent args)
|
||||
{
|
||||
// Check if speaker is enabled
|
||||
if (!component.Enabled)
|
||||
return;
|
||||
|
||||
// Check if speaker has power (if required)
|
||||
if (component.RequiresPower)
|
||||
{
|
||||
if (!TryComp<ApcPowerReceiverComponent>(speakerUid, out var powerReceiver) || !powerReceiver.Powered)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isEnabled)
|
||||
return;
|
||||
|
||||
byte[]? soundData = args.GeneratedTts;
|
||||
if (soundData == null || soundData.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var speakerPos = Transform(speakerUid).Coordinates;
|
||||
var playersInRange = Filter.Empty();
|
||||
|
||||
var playerQuery = EntityQueryEnumerator<ActorComponent, TransformComponent>();
|
||||
while (playerQuery.MoveNext(out var playerUid, out var actor, out var playerTransform))
|
||||
{
|
||||
if (speakerPos.TryDistance(EntityManager, playerTransform.Coordinates, out var distance) &&
|
||||
distance <= component.Range)
|
||||
{
|
||||
playersInRange = playersInRange.AddPlayer(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
|
||||
// Send TTS to players in range, excluding those who have disabled TTS
|
||||
var filteredPlayers = playersInRange.RemovePlayers(_ignoredRecipients);
|
||||
if (filteredPlayers.Recipients.Any())
|
||||
{
|
||||
var speakerNetEntity = GetNetEntity(speakerUid);
|
||||
RaiseNetworkEvent(new PlayTTSEvent(soundData, speakerNetEntity, isRadio: false), filteredPlayers);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySpokeEvent args)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Shared._Sunrise.AnnouncementSpeaker.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Marks an entity as a speaker that can broadcast station-wide announcements.
|
||||
/// Announcements will be played spatially from this speaker with the configured range.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class AnnouncementSpeakerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The range at which this speaker can be heard from.
|
||||
/// </summary>
|
||||
[DataField("range")]
|
||||
public float Range = 20f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this speaker is currently enabled.
|
||||
/// </summary>
|
||||
[DataField("enabled")]
|
||||
public bool Enabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// Volume modifier for announcements played through this speaker.
|
||||
/// </summary>
|
||||
[DataField("volumeModifier")]
|
||||
public float VolumeModifier = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this speaker requires power to function.
|
||||
/// </summary>
|
||||
[DataField("requiresPower")]
|
||||
public bool RequiresPower = true;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Shared._Sunrise.AnnouncementSpeaker.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a station-wide announcement should be played through speakers.
|
||||
/// This replaces the global broadcast system with a speaker-based network.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AnnouncementSpeakerEvent(
|
||||
EntityUid Station,
|
||||
string Message,
|
||||
ResolvedSoundSpecifier? AnnouncementSound,
|
||||
string? AnnounceVoice,
|
||||
byte[]? TtsData = null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on individual speakers to play an announcement.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct SpeakerPlayAnnouncementEvent(
|
||||
string Message,
|
||||
ResolvedSoundSpecifier? AnnouncementSound,
|
||||
string? AnnounceVoice,
|
||||
byte[]? GeneratedTts = null)
|
||||
{
|
||||
}
|
||||
|
|
@ -57,8 +57,11 @@ public sealed partial class SunriseCCVars : CVars
|
|||
public static readonly CVarDef<float> TTSRadioVolume =
|
||||
CVarDef.Create("tts.radio_volume", 0.50f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<float> TTSAnnounceVolume =
|
||||
CVarDef.Create("tts.announce_volume", 0.50f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
public static readonly CVarDef<string> TTSRadioEffect =
|
||||
CVarDef.Create("tts.radio_effect", "radio", CVar.SERVERONLY | CVar.ARCHIVE);
|
||||
|
||||
public static readonly CVarDef<string> TTSAnnounceEffect =
|
||||
CVarDef.Create("tts.announce_effect", "tiny_room", CVar.SERVERONLY | CVar.ARCHIVE);
|
||||
|
||||
/**
|
||||
* Ban Webhook
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.TTS;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AnnounceTtsEvent(byte[] data, ResolvedSoundSpecifier? announcementSound)
|
||||
: EntityEventArgs
|
||||
{
|
||||
public byte[] Data { get; } = data;
|
||||
public ResolvedSoundSpecifier? AnnouncementSound = announcementSound;
|
||||
}
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
abstract: true
|
||||
components:
|
||||
- type: StationAiWhitelist
|
||||
- type: AnnouncementSpeaker # Sunrise-Edit: Add announcement speaker capability
|
||||
range: 15
|
||||
enabled: true
|
||||
requiresPower: true
|
||||
- type: Electrified
|
||||
enabled: false
|
||||
usesApcPower: true
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
name: camera
|
||||
description: A surveillance camera. It's watching you. Kinda.
|
||||
components:
|
||||
- type: AnnouncementSpeaker # Sunrise-Edit: Add announcement speaker capability to cameras
|
||||
range: 12
|
||||
enabled: true
|
||||
requiresPower: true
|
||||
- type: Physics
|
||||
bodyType: Static
|
||||
- type: Fixtures
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue