using System.Collections.Generic; using System.Linq; using Content.Shared.Audio; using Content.Shared.CCVar; using Robust.Client.Player; using Robust.Shared.Audio; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Client.Audio { /// /// Samples nearby and plays audio. /// public sealed class AmbientSoundSystem : SharedAmbientSoundSystem { [Dependency] private IEntityLookup _lookup = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IRobustRandom _random = default!; private int _maxAmbientCount; private float _maxAmbientRange; private float _cooldown; private float _accumulator; /// /// How many times we can be playing 1 particular sound at once. /// private int _maxSingleSound = 3; private Dictionary _playingSounds = new(); private const float RangeBuffer = 0.5f; public override void Initialize() { base.Initialize(); var configManager = IoCManager.Resolve(); configManager.OnValueChanged(CCVars.AmbientCooldown, SetCooldown, true); configManager.OnValueChanged(CCVars.MaxAmbientSources, SetAmbientCount, true); configManager.OnValueChanged(CCVars.AmbientRange, SetAmbientRange, true); } 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(); var configManager = IoCManager.Resolve(); configManager.UnsubValueChanged(CCVars.AmbientCooldown, SetCooldown); configManager.UnsubValueChanged(CCVars.MaxAmbientSources, SetAmbientCount); configManager.UnsubValueChanged(CCVars.AmbientRange, SetAmbientRange); } private int PlayingCount(string countSound) { var count = 0; foreach (var (_, (_, sound)) in _playingSounds) { if (sound.Equals(countSound)) count++; } return count; } public override void Update(float frameTime) { base.Update(frameTime); if (!_gameTiming.IsFirstTimePredicted) return; if (_cooldown <= 0f) { _accumulator = 0f; return; } _accumulator += frameTime; if (_accumulator < _cooldown) return; _accumulator -= _cooldown; var player = _playerManager.LocalPlayer?.ControlledEntity; if (player == null) { ClearSounds(); return; } var coordinates = IoCManager.Resolve().GetComponent(player).Coordinates; foreach (var (comp, (stream, _)) in _playingSounds.ToArray()) { if (!comp.Deleted && comp.Enabled && IoCManager.Resolve().GetComponent(comp.Owner).Coordinates.TryDistance(EntityManager, coordinates, out var range) && range <= comp.Range) { continue; } stream?.Stop(); _playingSounds.Remove(comp); } if (_playingSounds.Count >= _maxAmbientCount) return; SampleNearby(coordinates); } private void ClearSounds() { foreach (var (_, (stream, _)) in _playingSounds) { stream?.Stop(); } _playingSounds.Clear(); } /// /// Get a list of ambient components in range and determine which ones to start playing. /// private void SampleNearby(EntityCoordinates coordinates) { var compsInRange = new List(); foreach (var entity in _lookup.GetEntitiesInRange(coordinates, _maxAmbientRange, LookupFlags.Approximate | LookupFlags.IncludeAnchored)) { if (!IoCManager.Resolve().TryGetComponent(entity, out AmbientSoundComponent? ambientComp) || _playingSounds.ContainsKey(ambientComp) || !ambientComp.Enabled || // We'll also do this crude distance check because it's what we're doing in the active loop above. !IoCManager.Resolve().GetComponent(entity).Coordinates.TryDistance(EntityManager, coordinates, out var range) || range > ambientComp.Range - RangeBuffer) { continue; } compsInRange.Add(ambientComp); } while (_playingSounds.Count < _maxAmbientCount) { if (compsInRange.Count == 0) break; var comp = _random.PickAndTake(compsInRange); var sound = comp.Sound.GetSound(); if (PlayingCount(sound) >= _maxSingleSound) continue; var audioParams = AudioHelpers .WithVariation(0.01f) .WithVolume(comp.Volume) .WithLoop(true) .WithAttenuation(Attenuation.LinearDistance) // Randomise start so 2 sources don't increase their volume. .WithPlayOffset(_random.NextFloat()) .WithMaxDistance(comp.Range); var stream = SoundSystem.Play( Filter.Local(), sound, comp.Owner, audioParams); if (stream == null) continue; _playingSounds[comp] = (stream, sound); } } } }