AmbientSoundSystem.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. using Content.Shared.Audio;
  2. using Content.Shared.CCVar;
  3. using Robust.Client.Graphics;
  4. using Robust.Client.Player;
  5. using Robust.Shared.Audio;
  6. using Robust.Shared.Log;
  7. using Robust.Shared.Configuration;
  8. using Robust.Shared.Map;
  9. using Robust.Shared.Physics;
  10. using Robust.Shared.Random;
  11. using Robust.Shared.Timing;
  12. using Robust.Shared.Utility;
  13. using System.Linq;
  14. using System.Numerics;
  15. using Robust.Client.GameObjects;
  16. using Robust.Shared.Audio.Effects;
  17. using Robust.Shared.Audio.Systems;
  18. using Robust.Shared.Player;
  19. namespace Content.Client.Audio;
  20. //TODO: This is using a incomplete version of the whole "only play nearest sounds" algo, that breaks down a bit should the ambient sound cap get hit.
  21. //TODO: This'll be fixed when GetEntitiesInRange produces consistent outputs.
  22. /// <summary>
  23. /// Samples nearby <see cref="AmbientSoundComponent"/> and plays audio.
  24. /// </summary>
  25. public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
  26. {
  27. [Dependency] private readonly AmbientSoundTreeSystem _treeSys = default!;
  28. [Dependency] private readonly SharedAudioSystem _audio = default!;
  29. [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
  30. [Dependency] private readonly IConfigurationManager _cfg = default!;
  31. [Dependency] private readonly IGameTiming _gameTiming = default!;
  32. [Dependency] private readonly IPlayerManager _playerManager = default!;
  33. [Dependency] private readonly IRobustRandom _random = default!;
  34. protected override void QueueUpdate(EntityUid uid, AmbientSoundComponent ambience)
  35. => _treeSys.QueueTreeUpdate(uid, ambience);
  36. private AmbientSoundOverlay? _overlay;
  37. private int _maxAmbientCount;
  38. private bool _overlayEnabled;
  39. private float _maxAmbientRange;
  40. private Vector2 MaxAmbientVector => new(_maxAmbientRange, _maxAmbientRange);
  41. private float _cooldown;
  42. private TimeSpan _targetTime = TimeSpan.Zero;
  43. private float _ambienceVolume = 0.0f;
  44. private static AudioParams _params = AudioParams.Default
  45. .WithVariation(0.01f)
  46. .WithLoop(true)
  47. .WithMaxDistance(7f);
  48. /// <summary>
  49. /// How many times we can be playing 1 particular sound at once.
  50. /// </summary>
  51. private int MaxSingleSound => (int) (_maxAmbientCount / (16.0f / 6.0f));
  52. private readonly Dictionary<Entity<AmbientSoundComponent>, (EntityUid? Stream, SoundSpecifier Sound, string Path)> _playingSounds = new();
  53. private readonly Dictionary<string, int> _playingCount = new();
  54. public bool OverlayEnabled
  55. {
  56. get => _overlayEnabled;
  57. set
  58. {
  59. if (_overlayEnabled == value) return;
  60. _overlayEnabled = value;
  61. var overlayManager = IoCManager.Resolve<IOverlayManager>();
  62. if (_overlayEnabled)
  63. {
  64. _overlay = new AmbientSoundOverlay(EntityManager, this, EntityManager.System<EntityLookupSystem>());
  65. overlayManager.AddOverlay(_overlay);
  66. }
  67. else
  68. {
  69. overlayManager.RemoveOverlay(_overlay!);
  70. _overlay = null;
  71. }
  72. }
  73. }
  74. /// <summary>
  75. /// Is this AmbientSound actively playing right now?
  76. /// </summary>
  77. /// <param name="component"></param>
  78. /// <returns></returns>
  79. public bool IsActive(Entity<AmbientSoundComponent> component)
  80. {
  81. return _playingSounds.ContainsKey(component);
  82. }
  83. public override void Initialize()
  84. {
  85. base.Initialize();
  86. UpdatesOutsidePrediction = true;
  87. UpdatesAfter.Add(typeof(AmbientSoundTreeSystem));
  88. Subs.CVar(_cfg, CCVars.AmbientCooldown, SetCooldown, true);
  89. Subs.CVar(_cfg, CCVars.MaxAmbientSources, SetAmbientCount, true);
  90. Subs.CVar(_cfg, CCVars.AmbientRange, SetAmbientRange, true);
  91. Subs.CVar(_cfg, CCVars.AmbienceVolume, SetAmbienceGain, true);
  92. SubscribeLocalEvent<AmbientSoundComponent, ComponentShutdown>(OnShutdown);
  93. }
  94. private void OnShutdown(EntityUid uid, AmbientSoundComponent component, ComponentShutdown args)
  95. {
  96. if (!_playingSounds.Remove((uid, component), out var sound))
  97. return;
  98. _audio.Stop(sound.Stream);
  99. _playingCount[sound.Path] -= 1;
  100. if (_playingCount[sound.Path] == 0)
  101. _playingCount.Remove(sound.Path);
  102. }
  103. private void SetAmbienceGain(float value)
  104. {
  105. _ambienceVolume = SharedAudioSystem.GainToVolume(value);
  106. foreach (var (ent, values) in _playingSounds)
  107. {
  108. if (values.Stream == null)
  109. continue;
  110. var stream = values.Stream;
  111. _audio.SetVolume(stream, _params.Volume + ent.Comp.Volume + _ambienceVolume);
  112. }
  113. }
  114. private void SetCooldown(float value) => _cooldown = value;
  115. private void SetAmbientCount(int value) => _maxAmbientCount = value;
  116. private void SetAmbientRange(float value) => _maxAmbientRange = value;
  117. public override void Shutdown()
  118. {
  119. base.Shutdown();
  120. ClearSounds();
  121. }
  122. private int PlayingCount(string countSound)
  123. {
  124. var count = 0;
  125. foreach (var (_, (_, sound, path)) in _playingSounds)
  126. {
  127. if (path.Equals(countSound))
  128. count++;
  129. }
  130. return count;
  131. }
  132. public override void Update(float frameTime)
  133. {
  134. base.Update(frameTime);
  135. if (!_gameTiming.IsFirstTimePredicted)
  136. return;
  137. if (_cooldown <= 0f)
  138. return;
  139. if (_gameTiming.CurTime < _targetTime)
  140. return;
  141. _targetTime = _gameTiming.CurTime + TimeSpan.FromSeconds(_cooldown);
  142. var player = _playerManager.LocalEntity;
  143. if (!EntityManager.TryGetComponent(player, out TransformComponent? xform))
  144. {
  145. ClearSounds();
  146. return;
  147. }
  148. ProcessNearbyAmbience(xform);
  149. }
  150. private void ClearSounds()
  151. {
  152. foreach (var (stream, _, _) in _playingSounds.Values)
  153. {
  154. _audio.Stop(stream);
  155. }
  156. _playingSounds.Clear();
  157. _playingCount.Clear();
  158. }
  159. private readonly struct QueryState
  160. {
  161. public readonly Dictionary<string, List<(float Importance, Entity<AmbientSoundComponent>)>> SourceDict = new();
  162. public readonly Vector2 MapPos;
  163. public readonly TransformComponent Player;
  164. public readonly SharedTransformSystem TransformSystem;
  165. public QueryState(Vector2 mapPos, TransformComponent player, SharedTransformSystem transformSystem)
  166. {
  167. MapPos = mapPos;
  168. Player = player;
  169. TransformSystem = transformSystem;
  170. }
  171. }
  172. private static bool Callback(
  173. ref QueryState state,
  174. in ComponentTreeEntry<AmbientSoundComponent> value)
  175. {
  176. var (ambientComp, xform) = value;
  177. DebugTools.Assert(ambientComp.Enabled);
  178. var delta = xform.ParentUid == state.Player.ParentUid
  179. ? xform.LocalPosition - state.Player.LocalPosition
  180. : state.TransformSystem.GetWorldPosition(xform) - state.MapPos;
  181. var range = delta.Length();
  182. if (range >= ambientComp.Range)
  183. return true;
  184. string key;
  185. if (ambientComp.Sound is SoundPathSpecifier path)
  186. key = path.Path.ToString();
  187. else
  188. key = ((SoundCollectionSpecifier)ambientComp.Sound).Collection ?? string.Empty;
  189. // Prioritize far away & loud sounds.
  190. var importance = range * (ambientComp.Volume + 32);
  191. state.SourceDict.GetOrNew(key).Add((importance, (value.Uid, ambientComp)));
  192. return true;
  193. }
  194. /// <summary>
  195. /// Get a list of ambient components in range and determine which ones to start playing.
  196. /// </summary>
  197. private void ProcessNearbyAmbience(TransformComponent playerXform)
  198. {
  199. var query = GetEntityQuery<TransformComponent>();
  200. var metaQuery = GetEntityQuery<MetaDataComponent>();
  201. var mapPos = _xformSystem.GetMapCoordinates(playerXform);
  202. // Remove out-of-range ambiences
  203. foreach (var (ent, sound) in _playingSounds)
  204. {
  205. //var entity = comp.Owner;
  206. var owner = ent.Owner;
  207. var comp = ent.Comp;
  208. if (comp.Enabled &&
  209. // Don't keep playing sounds that have changed since.
  210. sound.Sound == comp.Sound &&
  211. query.TryGetComponent(owner, out var xform) &&
  212. xform.MapID == playerXform.MapID &&
  213. !metaQuery.GetComponent(owner).EntityPaused)
  214. {
  215. // TODO: This is just trydistance for coordinates.
  216. var distance = (xform.ParentUid == playerXform.ParentUid)
  217. ? xform.LocalPosition - playerXform.LocalPosition
  218. : _xformSystem.GetWorldPosition(xform) - mapPos.Position;
  219. if (distance.LengthSquared() < comp.Range * comp.Range)
  220. continue;
  221. }
  222. _audio.Stop(sound.Stream);
  223. _playingSounds.Remove(ent);
  224. _playingCount[sound.Path] -= 1;
  225. if (_playingCount[sound.Path] == 0)
  226. _playingCount.Remove(sound.Path);
  227. }
  228. if (_playingSounds.Count >= _maxAmbientCount)
  229. return;
  230. var pos = mapPos.Position;
  231. var state = new QueryState(pos, playerXform, _xformSystem);
  232. var worldAabb = new Box2(pos - MaxAmbientVector, pos + MaxAmbientVector);
  233. _treeSys.QueryAabb(ref state, Callback, mapPos.MapId, worldAabb);
  234. // Add in range ambiences
  235. foreach (var (key, sourceList) in state.SourceDict)
  236. {
  237. if (_playingSounds.Count >= _maxAmbientCount)
  238. break;
  239. if (_playingCount.TryGetValue(key, out var playingCount) && playingCount >= MaxSingleSound)
  240. continue;
  241. sourceList.Sort(static (a, b) => b.Importance.CompareTo(a.Importance));
  242. foreach (var (_, sourceEntity) in sourceList)
  243. {
  244. var uid = sourceEntity.Owner;
  245. var comp = sourceEntity.Comp;
  246. if (_playingSounds.ContainsKey(sourceEntity) ||
  247. metaQuery.GetComponent(uid).EntityPaused)
  248. continue;
  249. var audioParams = _params
  250. .AddVolume(comp.Volume + _ambienceVolume)
  251. // Randomise start so 2 sources don't increase their volume.
  252. .WithPlayOffset(_random.NextFloat(0.0f, 100.0f))
  253. .WithMaxDistance(comp.Range);
  254. var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
  255. if (stream == null)
  256. continue;
  257. _playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key);
  258. playingCount++;
  259. if (_playingSounds.Count >= _maxAmbientCount)
  260. break;
  261. }
  262. if (playingCount != 0)
  263. _playingCount[key] = playingCount;
  264. }
  265. DebugTools.Assert(_playingCount.All(x => x.Value == PlayingCount(x.Key)));
  266. }
  267. }