ContentAudioSystem.LobbyMusic.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. using System.Linq;
  2. using Content.Client.GameTicking.Managers;
  3. using Content.Client.Lobby;
  4. using Content.Shared.Audio.Events;
  5. using Content.Shared.CCVar;
  6. using Content.Shared.GameTicking;
  7. using Robust.Client;
  8. using Robust.Client.ResourceManagement;
  9. using Robust.Client.State;
  10. using Robust.Shared.Audio;
  11. using Robust.Shared.Audio.Systems;
  12. using Robust.Shared.Player;
  13. using Robust.Shared.Timing;
  14. using Robust.Shared.Utility;
  15. namespace Content.Client.Audio;
  16. // Part of ContentAudioSystem that is responsible for lobby music playing/stopping and round-end sound-effect.
  17. public sealed partial class ContentAudioSystem
  18. {
  19. [Dependency] private readonly IBaseClient _client = default!;
  20. [Dependency] private readonly ClientGameTicker _gameTicker = default!;
  21. [Dependency] private readonly IResourceCache _resourceCache = default!;
  22. private readonly AudioParams _lobbySoundtrackParams = new(-5f, 1, 0, 0, 0, false, 0f);
  23. private readonly AudioParams _roundEndSoundEffectParams = new(-5f, 1, 0, 0, 0, false, 0f);
  24. /// <summary>
  25. /// EntityUid of lobby restart sound component.
  26. /// </summary>
  27. private EntityUid? _lobbyRoundRestartAudioStream;
  28. /// <summary>
  29. /// Shuffled list of soundtrack file-names.
  30. /// </summary>
  31. private string[]? _lobbyPlaylist;
  32. /// <summary>
  33. /// Short info about lobby soundtrack currently playing. Is null if soundtrack is not playing.
  34. /// </summary>
  35. private LobbySoundtrackInfo? _lobbySoundtrackInfo;
  36. private Action<LobbySoundtrackChangedEvent>? _lobbySoundtrackChanged;
  37. /// <summary>
  38. /// Event for subscription on lobby soundtrack changes.
  39. /// </summary>
  40. public event Action<LobbySoundtrackChangedEvent>? LobbySoundtrackChanged
  41. {
  42. add
  43. {
  44. if (value != null)
  45. {
  46. if (_lobbySoundtrackInfo != null)
  47. {
  48. value(new LobbySoundtrackChangedEvent(_lobbySoundtrackInfo.Filename));
  49. }
  50. _lobbySoundtrackChanged += value;
  51. }
  52. }
  53. remove => _lobbySoundtrackChanged -= value;
  54. }
  55. /// <summary>
  56. /// Initializes subscriptions that are related to lobby music.
  57. /// </summary>
  58. private void InitializeLobbyMusic()
  59. {
  60. Subs.CVar(_configManager, CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged);
  61. Subs.CVar(_configManager, CCVars.LobbyMusicVolume, LobbyMusicVolumeCVarChanged);
  62. _state.OnStateChanged += StateManagerOnStateChanged;
  63. _client.PlayerLeaveServer += OnLeave;
  64. SubscribeNetworkEvent<LobbyMusicStopEvent>(OnLobbySongStopped);
  65. SubscribeNetworkEvent<LobbyPlaylistChangedEvent>(OnLobbySongChanged);
  66. }
  67. private void OnLobbySongStopped(LobbyMusicStopEvent ev)
  68. {
  69. EndLobbyMusic();
  70. }
  71. private void StateManagerOnStateChanged(StateChangedEventArgs args)
  72. {
  73. switch (args.NewState)
  74. {
  75. case LobbyState:
  76. StartLobbyMusic();
  77. break;
  78. default:
  79. EndLobbyMusic();
  80. break;
  81. }
  82. }
  83. private void OnLeave(object? sender, PlayerEventArgs args)
  84. {
  85. EndLobbyMusic();
  86. }
  87. private void LobbyMusicVolumeCVarChanged(float volume)
  88. {
  89. if (_lobbySoundtrackInfo != null)
  90. {
  91. _audio.SetVolume(
  92. _lobbySoundtrackInfo.MusicStreamEntityUid,
  93. _lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))
  94. );
  95. }
  96. }
  97. private void LobbyMusicCVarChanged(bool musicEnabled)
  98. {
  99. if (musicEnabled && _state.CurrentState is LobbyState)
  100. {
  101. StartLobbyMusic();
  102. }
  103. else
  104. {
  105. EndLobbyMusic();
  106. }
  107. }
  108. private void OnLobbySongChanged(LobbyPlaylistChangedEvent playlistChangedEvent)
  109. {
  110. var playlist = playlistChangedEvent.Playlist;
  111. //playlist is already playing, no need to restart it
  112. if (_lobbySoundtrackInfo != null
  113. && _lobbyPlaylist != null
  114. && _lobbyPlaylist.SequenceEqual(playlist)
  115. )
  116. {
  117. return;
  118. }
  119. EndLobbyMusic();
  120. StartLobbyMusic(playlistChangedEvent.Playlist);
  121. }
  122. /// <summary>
  123. /// Re-starts playing lobby music from playlist, last sent from server. if there is currently none - does nothing.
  124. /// </summary>
  125. private void StartLobbyMusic()
  126. {
  127. if (_lobbyPlaylist == null || _lobbyPlaylist.Length == 0)
  128. {
  129. return;
  130. }
  131. StartLobbyMusic(_lobbyPlaylist);
  132. }
  133. /// <summary>
  134. /// Starts playing lobby music from playlist. If playlist is empty, or lobby music setting is turned off - does nothing.
  135. /// </summary>
  136. /// <param name="playlist">Array of soundtrack filenames for lobby playlist.</param>
  137. private void StartLobbyMusic(string[] playlist)
  138. {
  139. if (_lobbySoundtrackInfo != null || !_configManager.GetCVar(CCVars.LobbyMusicEnabled))
  140. return;
  141. _lobbyPlaylist = playlist;
  142. if (_lobbyPlaylist.Length == 0)
  143. {
  144. return;
  145. }
  146. PlaySoundtrack(playlist[0]);
  147. }
  148. private void PlaySoundtrack(string soundtrackFilename)
  149. {
  150. if (!_resourceCache.TryGetResource(new ResPath(soundtrackFilename), out AudioResource? audio))
  151. {
  152. return;
  153. }
  154. var playResult = _audio.PlayGlobal(
  155. soundtrackFilename,
  156. Filter.Local(),
  157. false,
  158. _lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
  159. );
  160. if (playResult == null)
  161. {
  162. _sawmill.Warning(
  163. $"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!",
  164. soundtrackFilename);
  165. return;
  166. }
  167. var nextTrackOn = _timing.CurTime + audio.AudioStream.Length;
  168. _lobbySoundtrackInfo = new LobbySoundtrackInfo(soundtrackFilename, nextTrackOn, playResult.Value.Entity);
  169. var lobbySongChangedEvent = new LobbySoundtrackChangedEvent(soundtrackFilename);
  170. _lobbySoundtrackChanged?.Invoke(lobbySongChangedEvent);
  171. }
  172. private void EndLobbyMusic()
  173. {
  174. if (_lobbySoundtrackInfo == null)
  175. {
  176. return;
  177. }
  178. _audio.Stop(_lobbySoundtrackInfo.MusicStreamEntityUid);
  179. _lobbySoundtrackInfo = null;
  180. var lobbySongChangedEvent = new LobbySoundtrackChangedEvent();
  181. _lobbySoundtrackChanged?.Invoke(lobbySongChangedEvent);
  182. }
  183. private void PlayRestartSound(RoundRestartCleanupEvent ev)
  184. {
  185. if (!_configManager.GetCVar(CCVars.RestartSoundsEnabled))
  186. return;
  187. var file = _gameTicker.RestartSound;
  188. if (ResolvedSoundSpecifier.IsNullOrEmpty(file))
  189. {
  190. return;
  191. }
  192. _lobbyRoundRestartAudioStream = _audio.PlayGlobal(
  193. file,
  194. Filter.Local(),
  195. false,
  196. _roundEndSoundEffectParams.WithVolume(_roundEndSoundEffectParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
  197. )?.Entity;
  198. }
  199. private void ShutdownLobbyMusic()
  200. {
  201. _state.OnStateChanged -= StateManagerOnStateChanged;
  202. _client.PlayerLeaveServer -= OnLeave;
  203. EndLobbyMusic();
  204. }
  205. private void UpdateLobbyMusic()
  206. {
  207. if (
  208. _lobbySoundtrackInfo != null
  209. && _timing.CurTime >= _lobbySoundtrackInfo.NextTrackOn
  210. && _lobbyPlaylist?.Length > 0
  211. )
  212. {
  213. var nextSoundtrackFilename = GetNextSoundtrackFromPlaylist(_lobbySoundtrackInfo.Filename, _lobbyPlaylist);
  214. PlaySoundtrack(nextSoundtrackFilename);
  215. }
  216. }
  217. private static string GetNextSoundtrackFromPlaylist(string currentSoundtrackFilename, string[] playlist)
  218. {
  219. var indexOfCurrent = Array.IndexOf(playlist, currentSoundtrackFilename);
  220. var nextTrackIndex = indexOfCurrent + 1;
  221. if (nextTrackIndex > playlist.Length - 1)
  222. {
  223. nextTrackIndex = 0;
  224. }
  225. return playlist[nextTrackIndex];
  226. }
  227. /// <summary> Container for lobby soundtrack information. </summary>
  228. /// <param name="Filename">Soundtrack filename.</param>
  229. /// <param name="NextTrackOn">Time (based on <see cref="IGameTiming.CurTime"/>) when this track is going to finish playing and next track have to be started.</param>
  230. /// <param name="MusicStreamEntityUid">
  231. /// EntityUid of launched soundtrack (from <see cref="SharedAudioSystem.PlayGlobal(string,Robust.Shared.Player.Filter,bool,System.Nullable{Robust.Shared.Audio.AudioParams})"/>).
  232. /// </param>
  233. private sealed record LobbySoundtrackInfo(string Filename, TimeSpan NextTrackOn, EntityUid MusicStreamEntityUid);
  234. }
  235. /// <summary>
  236. /// Event of changing lobby soundtrack (or stopping lobby music - will pass null for <paramref name="SoundtrackFilename"/> in that case).
  237. /// Is used by <see cref="ContentAudioSystem.LobbySoundtrackChanged"/> and <see cref="LobbyState.UpdateLobbySoundtrackInfo"/>.
  238. /// </summary>
  239. /// <param name="SoundtrackFilename">Filename of newly set soundtrack, or null if soundtrack playback is stopped.</param>
  240. public sealed record LobbySoundtrackChangedEvent(string? SoundtrackFilename = null);