using System.Linq; using Content.Client.GameTicking.Managers; using Content.Client.Lobby; using Content.Shared.Audio.Events; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Robust.Client; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Client.Audio; // Part of ContentAudioSystem that is responsible for lobby music playing/stopping and round-end sound-effect. public sealed partial class ContentAudioSystem { [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly ClientGameTicker _gameTicker = default!; [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; private readonly AudioParams _lobbySoundtrackParams = new(-5f, 1, 0, 0, 0, false, 0f); private readonly AudioParams _roundEndSoundEffectParams = new(-5f, 1, 0, 0, 0, false, 0f); /// /// EntityUid of lobby restart sound component. /// private EntityUid? _lobbyRoundRestartAudioStream; /// /// Shuffled list of soundtrack file-names. /// private string[]? _lobbyPlaylist; /// /// Short info about lobby soundtrack currently playing. Is null if soundtrack is not playing. /// private LobbySoundtrackInfo? _lobbySoundtrackInfo; private Action? _lobbySoundtrackChanged; /// /// Event for subscription on lobby soundtrack changes. /// public event Action? LobbySoundtrackChanged { add { if (value != null) { if (_lobbySoundtrackInfo != null) { value(new LobbySoundtrackChangedEvent(_lobbySoundtrackInfo.Filename)); } _lobbySoundtrackChanged += value; } } remove => _lobbySoundtrackChanged -= value; } /// /// Initializes subscriptions that are related to lobby music. /// private void InitializeLobbyMusic() { Subs.CVar(_configManager, CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged); Subs.CVar(_configManager, CCVars.LobbyMusicVolume, LobbyMusicVolumeCVarChanged); _stateManager.OnStateChanged += StateManagerOnStateChanged; _client.PlayerLeaveServer += OnLeave; SubscribeNetworkEvent(OnLobbySongStopped); SubscribeNetworkEvent(OnLobbySongChanged); } private void OnLobbySongStopped(LobbyMusicStopEvent ev) { EndLobbyMusic(); } private void StateManagerOnStateChanged(StateChangedEventArgs args) { switch (args.NewState) { case LobbyState: StartLobbyMusic(); break; default: EndLobbyMusic(); break; } } private void OnLeave(object? sender, PlayerEventArgs args) { EndLobbyMusic(); } private void LobbyMusicVolumeCVarChanged(float volume) { if (_lobbySoundtrackInfo != null) { _audio.SetVolume( _lobbySoundtrackInfo.MusicStreamEntityUid, _lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)) ); } } private void LobbyMusicCVarChanged(bool musicEnabled) { if (musicEnabled && _stateManager.CurrentState is LobbyState) { StartLobbyMusic(); } else { EndLobbyMusic(); } } private void OnLobbySongChanged(LobbyPlaylistChangedEvent playlistChangedEvent) { var playlist = playlistChangedEvent.Playlist; //playlist is already playing, no need to restart it if (_lobbySoundtrackInfo != null && _lobbyPlaylist != null && _lobbyPlaylist.SequenceEqual(playlist) ) { return; } EndLobbyMusic(); StartLobbyMusic(playlistChangedEvent.Playlist); } /// /// Re-starts playing lobby music from playlist, last sent from server. if there is currently none - does nothing. /// private void StartLobbyMusic() { if (_lobbyPlaylist == null || _lobbyPlaylist.Length == 0) { return; } StartLobbyMusic(_lobbyPlaylist); } /// /// Starts playing lobby music from playlist. If playlist is empty, or lobby music setting is turned off - does nothing. /// /// Array of soundtrack filenames for lobby playlist. private void StartLobbyMusic(string[] playlist) { if (_lobbySoundtrackInfo != null || !_configManager.GetCVar(CCVars.LobbyMusicEnabled)) return; _lobbyPlaylist = playlist; if (_lobbyPlaylist.Length == 0) { return; } PlaySoundtrack(playlist[0]); } private void PlaySoundtrack(string soundtrackFilename) { if (!_resourceCache.TryGetResource(new ResPath(soundtrackFilename), out AudioResource? audio)) { return; } var playResult = _audio.PlayGlobal( soundtrackFilename, Filter.Local(), false, _lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))) ); if (playResult.Value.Entity == default) { _sawmill.Warning( $"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!", soundtrackFilename); return; } var nextTrackOn = _timing.CurTime + audio.AudioStream.Length; _lobbySoundtrackInfo = new LobbySoundtrackInfo(soundtrackFilename, nextTrackOn, playResult.Value.Entity); var lobbySongChangedEvent = new LobbySoundtrackChangedEvent(soundtrackFilename); _lobbySoundtrackChanged?.Invoke(lobbySongChangedEvent); } private void EndLobbyMusic() { if (_lobbySoundtrackInfo == null) { return; } _audio.Stop(_lobbySoundtrackInfo.MusicStreamEntityUid); _lobbySoundtrackInfo = null; var lobbySongChangedEvent = new LobbySoundtrackChangedEvent(); _lobbySoundtrackChanged?.Invoke(lobbySongChangedEvent); } private void PlayRestartSound(RoundRestartCleanupEvent ev) { if (!_configManager.GetCVar(CCVars.RestartSoundsEnabled)) return; var file = _gameTicker.RestartSound; if (string.IsNullOrEmpty(file)) { return; } _lobbyRoundRestartAudioStream = _audio.PlayGlobal( file, Filter.Local(), false, _roundEndSoundEffectParams.WithVolume(_roundEndSoundEffectParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))) )?.Entity; } private void ShutdownLobbyMusic() { _stateManager.OnStateChanged -= StateManagerOnStateChanged; _client.PlayerLeaveServer -= OnLeave; EndLobbyMusic(); } private void UpdateLobbyMusic() { if ( _lobbySoundtrackInfo != null && _timing.CurTime >= _lobbySoundtrackInfo.NextTrackOn && _lobbyPlaylist?.Length > 0 ) { var nextSoundtrackFilename = GetNextSoundtrackFromPlaylist(_lobbySoundtrackInfo.Filename, _lobbyPlaylist); PlaySoundtrack(nextSoundtrackFilename); } } private static string GetNextSoundtrackFromPlaylist(string currentSoundtrackFilename, string[] playlist) { var indexOfCurrent = Array.IndexOf(playlist, currentSoundtrackFilename); var nextTrackIndex = indexOfCurrent + 1; if (nextTrackIndex > playlist.Length - 1) { nextTrackIndex = 0; } return playlist[nextTrackIndex]; } /// Container for lobby soundtrack information. /// Soundtrack filename. /// Time (based on ) when this track is going to finish playing and next track have to be started. /// /// EntityUid of launched soundtrack (from ). /// private sealed record LobbySoundtrackInfo(string Filename, TimeSpan NextTrackOn, EntityUid MusicStreamEntityUid); } /// /// Event of changing lobby soundtrack (or stopping lobby music - will pass null for in that case). /// Is used by and . /// /// Filename of newly set soundtrack, or null if soundtrack playback is stopped. public sealed record LobbySoundtrackChangedEvent(string? SoundtrackFilename = null);