| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- using Content.Shared.Audio;
- using Content.Shared.CCVar;
- using Robust.Client.Graphics;
- using Robust.Client.Player;
- using Robust.Shared.Audio;
- using Robust.Shared.Log;
- using Robust.Shared.Configuration;
- using Robust.Shared.Map;
- using Robust.Shared.Physics;
- using Robust.Shared.Random;
- using Robust.Shared.Timing;
- using Robust.Shared.Utility;
- using System.Linq;
- using System.Numerics;
- using Robust.Client.GameObjects;
- using Robust.Shared.Audio.Effects;
- using Robust.Shared.Audio.Systems;
- using Robust.Shared.Player;
- namespace Content.Client.Audio;
- //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.
- //TODO: This'll be fixed when GetEntitiesInRange produces consistent outputs.
- /// <summary>
- /// Samples nearby <see cref="AmbientSoundComponent"/> and plays audio.
- /// </summary>
- public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
- {
- [Dependency] private readonly AmbientSoundTreeSystem _treeSys = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- protected override void QueueUpdate(EntityUid uid, AmbientSoundComponent ambience)
- => _treeSys.QueueTreeUpdate(uid, ambience);
- private AmbientSoundOverlay? _overlay;
- private int _maxAmbientCount;
- private bool _overlayEnabled;
- private float _maxAmbientRange;
- private Vector2 MaxAmbientVector => new(_maxAmbientRange, _maxAmbientRange);
- private float _cooldown;
- private TimeSpan _targetTime = TimeSpan.Zero;
- private float _ambienceVolume = 0.0f;
- private static AudioParams _params = AudioParams.Default
- .WithVariation(0.01f)
- .WithLoop(true)
- .WithMaxDistance(7f);
- /// <summary>
- /// How many times we can be playing 1 particular sound at once.
- /// </summary>
- private int MaxSingleSound => (int) (_maxAmbientCount / (16.0f / 6.0f));
- private readonly Dictionary<Entity<AmbientSoundComponent>, (EntityUid? Stream, SoundSpecifier Sound, string Path)> _playingSounds = new();
- private readonly Dictionary<string, int> _playingCount = new();
- public bool OverlayEnabled
- {
- get => _overlayEnabled;
- set
- {
- if (_overlayEnabled == value) return;
- _overlayEnabled = value;
- var overlayManager = IoCManager.Resolve<IOverlayManager>();
- if (_overlayEnabled)
- {
- _overlay = new AmbientSoundOverlay(EntityManager, this, EntityManager.System<EntityLookupSystem>());
- overlayManager.AddOverlay(_overlay);
- }
- else
- {
- overlayManager.RemoveOverlay(_overlay!);
- _overlay = null;
- }
- }
- }
- /// <summary>
- /// Is this AmbientSound actively playing right now?
- /// </summary>
- /// <param name="component"></param>
- /// <returns></returns>
- public bool IsActive(Entity<AmbientSoundComponent> component)
- {
- return _playingSounds.ContainsKey(component);
- }
- public override void Initialize()
- {
- base.Initialize();
- UpdatesOutsidePrediction = true;
- UpdatesAfter.Add(typeof(AmbientSoundTreeSystem));
- Subs.CVar(_cfg, CCVars.AmbientCooldown, SetCooldown, true);
- Subs.CVar(_cfg, CCVars.MaxAmbientSources, SetAmbientCount, true);
- Subs.CVar(_cfg, CCVars.AmbientRange, SetAmbientRange, true);
- Subs.CVar(_cfg, CCVars.AmbienceVolume, SetAmbienceGain, true);
- SubscribeLocalEvent<AmbientSoundComponent, ComponentShutdown>(OnShutdown);
- }
- private void OnShutdown(EntityUid uid, AmbientSoundComponent component, ComponentShutdown args)
- {
- if (!_playingSounds.Remove((uid, component), out var sound))
- return;
- _audio.Stop(sound.Stream);
- _playingCount[sound.Path] -= 1;
- if (_playingCount[sound.Path] == 0)
- _playingCount.Remove(sound.Path);
- }
- private void SetAmbienceGain(float value)
- {
- _ambienceVolume = SharedAudioSystem.GainToVolume(value);
- foreach (var (ent, values) in _playingSounds)
- {
- if (values.Stream == null)
- continue;
- var stream = values.Stream;
- _audio.SetVolume(stream, _params.Volume + ent.Comp.Volume + _ambienceVolume);
- }
- }
- private void SetCooldown(float value) => _cooldown = value;
- private void SetAmbientCount(int value) => _maxAmbientCount = value;
- private void SetAmbientRange(float value) => _maxAmbientRange = value;
- public override void Shutdown()
- {
- base.Shutdown();
- ClearSounds();
- }
- private int PlayingCount(string countSound)
- {
- var count = 0;
- foreach (var (_, (_, sound, path)) in _playingSounds)
- {
- if (path.Equals(countSound))
- count++;
- }
- return count;
- }
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- if (!_gameTiming.IsFirstTimePredicted)
- return;
- if (_cooldown <= 0f)
- return;
- if (_gameTiming.CurTime < _targetTime)
- return;
- _targetTime = _gameTiming.CurTime + TimeSpan.FromSeconds(_cooldown);
- var player = _playerManager.LocalEntity;
- if (!EntityManager.TryGetComponent(player, out TransformComponent? xform))
- {
- ClearSounds();
- return;
- }
- ProcessNearbyAmbience(xform);
- }
- private void ClearSounds()
- {
- foreach (var (stream, _, _) in _playingSounds.Values)
- {
- _audio.Stop(stream);
- }
- _playingSounds.Clear();
- _playingCount.Clear();
- }
- private readonly struct QueryState
- {
- public readonly Dictionary<string, List<(float Importance, Entity<AmbientSoundComponent>)>> SourceDict = new();
- public readonly Vector2 MapPos;
- public readonly TransformComponent Player;
- public readonly SharedTransformSystem TransformSystem;
- public QueryState(Vector2 mapPos, TransformComponent player, SharedTransformSystem transformSystem)
- {
- MapPos = mapPos;
- Player = player;
- TransformSystem = transformSystem;
- }
- }
- private static bool Callback(
- ref QueryState state,
- in ComponentTreeEntry<AmbientSoundComponent> value)
- {
- var (ambientComp, xform) = value;
- DebugTools.Assert(ambientComp.Enabled);
- var delta = xform.ParentUid == state.Player.ParentUid
- ? xform.LocalPosition - state.Player.LocalPosition
- : state.TransformSystem.GetWorldPosition(xform) - state.MapPos;
- var range = delta.Length();
- if (range >= ambientComp.Range)
- return true;
- string key;
- if (ambientComp.Sound is SoundPathSpecifier path)
- key = path.Path.ToString();
- else
- key = ((SoundCollectionSpecifier)ambientComp.Sound).Collection ?? string.Empty;
- // Prioritize far away & loud sounds.
- var importance = range * (ambientComp.Volume + 32);
- state.SourceDict.GetOrNew(key).Add((importance, (value.Uid, ambientComp)));
- return true;
- }
- /// <summary>
- /// Get a list of ambient components in range and determine which ones to start playing.
- /// </summary>
- private void ProcessNearbyAmbience(TransformComponent playerXform)
- {
- var query = GetEntityQuery<TransformComponent>();
- var metaQuery = GetEntityQuery<MetaDataComponent>();
- var mapPos = _xformSystem.GetMapCoordinates(playerXform);
- // Remove out-of-range ambiences
- foreach (var (ent, sound) in _playingSounds)
- {
- //var entity = comp.Owner;
- var owner = ent.Owner;
- var comp = ent.Comp;
- if (comp.Enabled &&
- // Don't keep playing sounds that have changed since.
- sound.Sound == comp.Sound &&
- query.TryGetComponent(owner, out var xform) &&
- xform.MapID == playerXform.MapID &&
- !metaQuery.GetComponent(owner).EntityPaused)
- {
- // TODO: This is just trydistance for coordinates.
- var distance = (xform.ParentUid == playerXform.ParentUid)
- ? xform.LocalPosition - playerXform.LocalPosition
- : _xformSystem.GetWorldPosition(xform) - mapPos.Position;
- if (distance.LengthSquared() < comp.Range * comp.Range)
- continue;
- }
- _audio.Stop(sound.Stream);
- _playingSounds.Remove(ent);
- _playingCount[sound.Path] -= 1;
- if (_playingCount[sound.Path] == 0)
- _playingCount.Remove(sound.Path);
- }
- if (_playingSounds.Count >= _maxAmbientCount)
- return;
- var pos = mapPos.Position;
- var state = new QueryState(pos, playerXform, _xformSystem);
- var worldAabb = new Box2(pos - MaxAmbientVector, pos + MaxAmbientVector);
- _treeSys.QueryAabb(ref state, Callback, mapPos.MapId, worldAabb);
- // Add in range ambiences
- foreach (var (key, sourceList) in state.SourceDict)
- {
- if (_playingSounds.Count >= _maxAmbientCount)
- break;
- if (_playingCount.TryGetValue(key, out var playingCount) && playingCount >= MaxSingleSound)
- continue;
- sourceList.Sort(static (a, b) => b.Importance.CompareTo(a.Importance));
- foreach (var (_, sourceEntity) in sourceList)
- {
- var uid = sourceEntity.Owner;
- var comp = sourceEntity.Comp;
- if (_playingSounds.ContainsKey(sourceEntity) ||
- metaQuery.GetComponent(uid).EntityPaused)
- continue;
- var audioParams = _params
- .AddVolume(comp.Volume + _ambienceVolume)
- // Randomise start so 2 sources don't increase their volume.
- .WithPlayOffset(_random.NextFloat(0.0f, 100.0f))
- .WithMaxDistance(comp.Range);
- var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
- if (stream == null)
- continue;
- _playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key);
- playingCount++;
- if (_playingSounds.Count >= _maxAmbientCount)
- break;
- }
- if (playingCount != 0)
- _playingCount[key] = playingCount;
- }
- DebugTools.Assert(_playingCount.All(x => x.Value == PlayingCount(x.Key)));
- }
- }
|