diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs index 19dd6ecb1e..48a3c0bda4 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -39,14 +39,22 @@ namespace Content.Client.Audio private TimeSpan _targetTime = TimeSpan.Zero; private float _ambienceVolume = 0.0f; + // Note that except for some rare exceptions, every ambient sound source appears to be static. So: + // TODO AMBIENT SOUND Use only static queries + // This would make all the lookups significantly faster. There are some rare exceptions, like flies, vehicles, + // and the singularity. But those can just play sounds via some other system. Alternatively: give ambient sound + // its own client-side tree to avoid this issue altogether. + private static LookupFlags _flags = LookupFlags.Static | LookupFlags.Dynamic | LookupFlags.Sundries; + + private static AudioParams _params = AudioParams.Default.WithVariation(0.01f).WithLoop(true).WithAttenuation(Attenuation.LinearDistance); + /// /// How many times we can be playing 1 particular sound at once. /// private int MaxSingleSound => (int) (_maxAmbientCount / (16.0f / 6.0f)); private readonly Dictionary _playingSounds = new(); - - private const float RangeBuffer = 3f; + private readonly Dictionary _playingCount = new(); public bool OverlayEnabled { @@ -94,8 +102,13 @@ namespace Content.Client.Audio private void OnShutdown(EntityUid uid, AmbientSoundComponent component, ComponentShutdown args) { - if (_playingSounds.Remove(component, out var sound)) - sound.Stream?.Stop(); + if (!_playingSounds.Remove(component, out var sound)) + return; + + sound.Stream?.Stop(); + _playingCount[sound.Sound] -= 1; + if (_playingCount[sound.Sound] == 0) + _playingCount.Remove(sound.Sound); } private void SetAmbienceVolume(float value) => _ambienceVolume = value; @@ -143,15 +156,13 @@ namespace Content.Client.Audio _targetTime = _gameTiming.CurTime+TimeSpan.FromSeconds(_cooldown); var player = _playerManager.LocalPlayer?.ControlledEntity; - if (!EntityManager.TryGetComponent(player, out TransformComponent? playerManager)) + if (!EntityManager.TryGetComponent(player, out TransformComponent? xform)) { ClearSounds(); return; } - var coordinates = playerManager.Coordinates; - - ProcessNearbyAmbience(coordinates); + ProcessNearbyAmbience(xform); } private void ClearSounds() @@ -164,36 +175,37 @@ namespace Content.Client.Audio _playingSounds.Clear(); } - private Dictionary> GetNearbySources(EntityCoordinates coordinates) + private Dictionary> GetNearbySources(TransformComponent playerXform, MapCoordinates coords, EntityQuery xformQuery) { - //TODO: Make this produce a hashset of nearby entities again. - var sourceDict = new Dictionary>(16); + var sourceDict = new Dictionary>(16); var ambientQuery = GetEntityQuery(); - var xformQuery = GetEntityQuery(); - foreach (var entity in _lookup.GetEntitiesInRange(coordinates, _maxAmbientRange + RangeBuffer)) + // TODO add variant of GetComponentsInRange that also returns the transform component. + foreach (var entity in _lookup.GetEntitiesInRange(coords, _maxAmbientRange, flags: _flags)) { - if (!ambientQuery.TryGetComponent(entity, out var ambientComp) || - !ambientComp.Enabled || - !xformQuery.GetComponent(entity).Coordinates.TryDistance(EntityManager, coordinates, out var range) || - range > ambientComp.Range) - { + if (!ambientQuery.TryGetComponent(entity, out var ambientComp) || !ambientComp.Enabled) continue; - } - var key = _audio.GetSound(ambientComp.Sound); + var xform = xformQuery.GetComponent(entity); + var delta = xform.ParentUid == playerXform.ParentUid + ? xform.LocalPosition - playerXform.LocalPosition + : xform.WorldPosition - coords.Position; - if (!sourceDict.ContainsKey(key)) - sourceDict[key] = new List(MaxSingleSound); + var range = delta.Length; + if (range >= ambientComp.Range) + continue; - sourceDict[key].Add(ambientComp); - } + string key; - // TODO: Just store the distance from above... - foreach (var (key, val) in sourceDict) - { - sourceDict[key] = val.OrderByDescending(x => - Transform(x.Owner).Coordinates.TryDistance(EntityManager, coordinates, out var dist) ? dist * (x.Volume + 32) : float.MaxValue).ToList(); + if (ambientComp.Sound is SoundPathSpecifier path) + key = path.Path?.ToString() ?? string.Empty; + else + key = ((SoundCollectionSpecifier) ambientComp.Sound).Collection ?? string.Empty; + + var list = sourceDict.GetOrNew(key); + + // Prioritize far away & loud sounds. + list.Add(-range * (ambientComp.Volume + 32), ambientComp); } return sourceDict; @@ -202,98 +214,68 @@ namespace Content.Client.Audio /// /// Get a list of ambient components in range and determine which ones to start playing. /// - private void ProcessNearbyAmbience(EntityCoordinates coordinates) + private void ProcessNearbyAmbience(TransformComponent playerXform) { - var compsInRange= GetNearbySources(coordinates); + var query = GetEntityQuery(); + var mapPos = playerXform.MapPosition; - var keys = compsInRange.Keys.ToHashSet(); - - while (keys.Count != 0) + // Remove out-of-range ambiences + foreach (var (comp, sound) in _playingSounds) { - if (_playingSounds.Count >= _maxAmbientCount) + var entity = comp.Owner; + if (comp.Enabled && query.TryGetComponent(entity, out var xform) && xform.MapID == playerXform.MapID) { - /* - // Go through and remove everything from compSet - foreach (var toRemove in keys.SelectMany(key => compsInRange[key])) - { - compSet.Remove(toRemove.Owner); - } - */ + var distance = (xform.ParentUid == playerXform.ParentUid) + ? xform.LocalPosition - playerXform.LocalPosition + : xform.WorldPosition - mapPos.Position; - break; + if (distance.LengthSquared < comp.Range * comp.Range) + continue; } - foreach (var key in keys) + sound.Stream?.Stop(); + _playingSounds.Remove(comp); + _playingCount[sound.Sound] -= 1; + if (_playingCount[sound.Sound] == 0) + _playingCount.Remove(sound.Sound); + } + + if (_playingSounds.Count >= _maxAmbientCount) + return; + + // Add in range ambiences + foreach (var (key, sources) in GetNearbySources(playerXform, mapPos, query)) + { + if (_playingSounds.Count >= _maxAmbientCount) + break; + + if (_playingCount.TryGetValue(key, out var playingCount) && playingCount >= MaxSingleSound) + continue; + + foreach (var comp in sources.Values) { - if (_playingSounds.Count >= _maxAmbientCount) - break; - - if (compsInRange[key].Count == 0) - { - keys.Remove(key); - continue; - } - - var comp = compsInRange[key].Pop(); if (_playingSounds.ContainsKey(comp)) continue; - var sound = _audio.GetSound(comp.Sound); - - if (PlayingCount(sound) >= MaxSingleSound) - { - keys.Remove(key); - /*foreach (var toRemove in compsInRange[key]) - { - Logger.Debug($"removing {toRemove.Owner} from set."); - compSet.Remove(toRemove.Owner); - }*/ - compsInRange[key].Clear(); // reduce work later should we overrun the max sounds. - continue; - } - - var audioParams = AudioParams.Default - .WithVariation(0.01f) - .WithVolume(comp.Volume + _ambienceVolume) - .WithLoop(true) - .WithAttenuation(Attenuation.LinearDistance) + 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.PlayPvs(comp.Sound, comp.Owner, audioParams); + if (stream != null) + _playingSounds[comp] = (stream, key); - if (stream == null) continue; - - _playingSounds[comp] = (stream, sound); + playingCount++; + if (_playingSounds.Count >= _maxAmbientCount) + break; } + + _playingCount[key] = playingCount; } - foreach (var (comp, sound) in _playingSounds) - { - var entity = comp.Owner; - if (comp.Deleted || // includes entity deletion - !comp.Enabled || - !EntityManager.GetComponent(entity).Coordinates - .TryDistance(EntityManager, coordinates, out var range) || - range > comp.Range) - { - sound.Stream?.Stop(); - _playingSounds.Remove(comp); - } - } - - //TODO: Put this code back in place! Currently not done this way because of GetEntitiesInRange being funny. - /* - foreach (var (comp, sound) in _playingSounds) - { - if (compSet.Contains(comp.Owner)) continue; - - Logger.Debug($"Cancelled {comp.Owner}"); - _playingSounds[comp].Stream?.Stop(); - _playingSounds.Remove(comp); - } - */ + DebugTools.Assert(_playingCount.All(x => x.Value == PlayingCount(x.Key))); } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index e74e1559b1..58c1198bc0 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -48,7 +48,7 @@ namespace Content.Shared.CCVar /// How large of a range to sample for ambience. /// public static readonly CVarDef AmbientRange = - CVarDef.Create("ambience.range", 5f, CVar.REPLICATED | CVar.SERVER); + CVarDef.Create("ambience.range", 8f, CVar.REPLICATED | CVar.SERVER); /// /// Maximum simultaneous ambient sounds.