diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs index b9d22e3a54..15ab04636a 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -3,8 +3,10 @@ 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; @@ -20,13 +22,16 @@ namespace Content.Client.Audio /// public sealed class AmbientSoundSystem : SharedAmbientSoundSystem { - [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly AmbientSoundTreeSystem _treeSys = default!; [Dependency] private readonly SharedAudioSystem _audio = 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; @@ -35,13 +40,6 @@ 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); /// @@ -172,40 +170,48 @@ namespace Content.Client.Audio _playingCount.Clear(); } - private Dictionary> GetNearbySources(TransformComponent playerXform, MapCoordinates coords, EntityQuery xformQuery) + private readonly struct QueryState { - var sourceDict = new Dictionary>(16); - var ambientQuery = GetEntityQuery(); + public readonly Dictionary> SourceDict = new(); + public readonly Vector2 MapPos; + public readonly TransformComponent Player; + public readonly EntityQuery Query; - // TODO add variant of GetComponentsInRange that also returns the transform component. - foreach (var entity in _lookup.GetEntitiesInRange(coords, _maxAmbientRange, flags: _flags)) + public QueryState(Vector2 mapPos, TransformComponent player, EntityQuery query) { - if (!ambientQuery.TryGetComponent(entity, out var ambientComp) || !ambientComp.Enabled) - continue; - - var xform = xformQuery.GetComponent(entity); - var delta = xform.ParentUid == playerXform.ParentUid - ? xform.LocalPosition - playerXform.LocalPosition - : xform.WorldPosition - coords.Position; - - var range = delta.Length; - if (range >= ambientComp.Range) - continue; - - string key; - - 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)); + MapPos = mapPos; + Player = player; + Query = query; } + } - return sourceDict; + private static bool Callback( + ref QueryState state, + in ComponentTreeEntry value) + { + var (ambientComp, xform) = value; + + DebugTools.Assert(ambientComp.Enabled); + + var delta = xform.ParentUid == state.Player.ParentUid + ? xform.LocalPosition - state.Player.LocalPosition + : xform.WorldPosition - state.MapPos; + + var range = delta.Length; + if (range >= ambientComp.Range) + return true; + + string key; + + if (ambientComp.Sound is SoundPathSpecifier path) + key = path.Path?.ToString() ?? string.Empty; + 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, ambientComp)); + return true; } /// @@ -240,8 +246,13 @@ namespace Content.Client.Audio if (_playingSounds.Count >= _maxAmbientCount) return; + var pos = mapPos.Position; + var state = new QueryState(pos, playerXform, query); + var worldAabb = new Box2(pos - _maxAmbientRange, pos + _maxAmbientRange); + _treeSys.QueryAabb(ref state, Callback, mapPos.MapId, worldAabb); + // Add in range ambiences - foreach (var (key, sources) in GetNearbySources(playerXform, mapPos, query)) + foreach (var (key, sources) in state.SourceDict) { if (_playingSounds.Count >= _maxAmbientCount) break; diff --git a/Content.Client/Audio/AmbientSoundTreeComponent.cs b/Content.Client/Audio/AmbientSoundTreeComponent.cs new file mode 100644 index 0000000000..a256b9f0b3 --- /dev/null +++ b/Content.Client/Audio/AmbientSoundTreeComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Audio; +using Robust.Shared.ComponentTrees; +using Robust.Shared.Physics; + +namespace Content.Client.Audio; + +/// +/// Samples nearby and plays audio. +/// +[RegisterComponent] +public sealed class AmbientSoundTreeComponent : Component, IComponentTreeComponent +{ + public DynamicTree> Tree { get; set; } = default!; +} diff --git a/Content.Client/Audio/AmbientSoundTreeSystem.cs b/Content.Client/Audio/AmbientSoundTreeSystem.cs new file mode 100644 index 0000000000..6e2fd58d72 --- /dev/null +++ b/Content.Client/Audio/AmbientSoundTreeSystem.cs @@ -0,0 +1,31 @@ +using Content.Shared.Audio; +using Robust.Shared.ComponentTrees; +using Robust.Shared.Physics; + +namespace Content.Client.Audio; + +public sealed class AmbientSoundTreeSystem : ComponentTreeSystem +{ + #region Component Tree Overrides + protected override bool DoFrameUpdate => false; + protected override bool DoTickUpdate => true; + protected override int InitialCapacity => 256; + protected override bool Recursive => true; + + protected override Box2 ExtractAabb(in ComponentTreeEntry entry, Vector2 pos, Angle rot) + => new Box2(pos - entry.Component.Range, pos + entry.Component.Range); + + protected override Box2 ExtractAabb(in ComponentTreeEntry entry) + { + if (entry.Component.TreeUid == null) + return default; + + var pos = XformSystem.GetRelativePosition( + entry.Transform, + entry.Component.TreeUid.Value, + GetEntityQuery()); + + return ExtractAabb(in entry, pos, default); + } + #endregion +} diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs index 83d565905b..5197f2ca36 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Server.Construction; using Content.Server.MachineLinking.Components; using Content.Server.MachineLinking.Events; @@ -32,6 +32,7 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambienntSound = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly ArtifactSystem _artifact = default!; @@ -475,11 +476,7 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem if (TryComp(uid, out var powa)) powa.NeedsPower = true; - if (TryComp(uid, out var ambientSound)) - { - ambientSound.Enabled = true; - Dirty(ambientSound); - } + _ambienntSound.SetAmbience(uid, true); } private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args) @@ -487,11 +484,7 @@ public sealed class ArtifactAnalyzerSystem : EntitySystem if (TryComp(uid, out var powa)) powa.NeedsPower = false; - if (TryComp(uid, out var ambientSound)) - { - ambientSound.Enabled = false; - Dirty(ambientSound); - } + _ambienntSound.SetAmbience(uid, false); } private void OnPowerChanged(EntityUid uid, ActiveArtifactAnalyzerComponent component, ref PowerChangedEvent args) diff --git a/Content.Shared/Audio/AmbientSoundComponent.cs b/Content.Shared/Audio/AmbientSoundComponent.cs index fa62380cd1..53867b5976 100644 --- a/Content.Shared/Audio/AmbientSoundComponent.cs +++ b/Content.Shared/Audio/AmbientSoundComponent.cs @@ -1,33 +1,44 @@ using Robust.Shared.Audio; +using Robust.Shared.ComponentTrees; using Robust.Shared.GameStates; +using Robust.Shared.Physics; using Robust.Shared.Serialization; namespace Content.Shared.Audio { [RegisterComponent] [NetworkedComponent] - public sealed class AmbientSoundComponent : Component + [Access(typeof(SharedAmbientSoundSystem))] + public sealed class AmbientSoundComponent : Component, IComponentTreeEntry { - [ViewVariables(VVAccess.ReadWrite)] [DataField("enabled")] + [ViewVariables(VVAccess.ReadWrite)] // only for map editing public bool Enabled { get; set; } = true; - [DataField("sound", required: true), ViewVariables(VVAccess.ReadWrite)] + [DataField("sound", required: true), ViewVariables(VVAccess.ReadWrite)] // only for map editing public SoundSpecifier Sound = default!; /// /// How far away this ambient sound can potentially be heard. /// - [ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadWrite)] // only for map editing [DataField("range")] public float Range = 2f; /// /// Applies this volume to the sound being played. /// - [ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadWrite)] // only for map editing [DataField("volume")] public float Volume = -10f; + + public EntityUid? TreeUid { get; set; } + + public DynamicTree>? Tree { get; set; } + + public bool AddToTree => Enabled; + + public bool TreeUpdateQueued { get; set; } } [Serializable, NetSerializable] diff --git a/Content.Shared/Audio/SharedAmbientSoundSystem.cs b/Content.Shared/Audio/SharedAmbientSoundSystem.cs index 5f86b647f0..c54e9589b2 100644 --- a/Content.Shared/Audio/SharedAmbientSoundSystem.cs +++ b/Content.Shared/Audio/SharedAmbientSoundSystem.cs @@ -11,24 +11,46 @@ namespace Content.Shared.Audio SubscribeLocalEvent(HandleCompState); } - public void SetAmbience(EntityUid uid, bool value) + public virtual void SetAmbience(EntityUid uid, bool value, AmbientSoundComponent? ambience = null) { - // Reason I didn't make this eventbus for the callers is because it seemed a bit silly - // trying to account for damageable + powered + toggle, plus we can't just check if it's powered. - // So we'll just call it directly for whatever. - if (!EntityManager.TryGetComponent(uid, out var ambience) || - ambience.Enabled == value) return; + if (!Resolve(uid, ref ambience, false) || ambience.Enabled == value) + return; ambience.Enabled = value; + QueueUpdate(uid, ambience); + Dirty(ambience); + } + + public virtual void SetRange(EntityUid uid, float value, AmbientSoundComponent? ambience = null) + { + if (!Resolve(uid, ref ambience, false) || MathHelper.CloseToPercent(ambience.Range, value)) + return; + + ambience.Range = value; + QueueUpdate(uid, ambience); + Dirty(ambience); + } + + protected virtual void QueueUpdate(EntityUid uid, AmbientSoundComponent ambience) + { + // client side tree + } + + public virtual void SetVolume(EntityUid uid, float value, AmbientSoundComponent? ambience = null) + { + if (!Resolve(uid, ref ambience, false) || MathHelper.CloseToPercent(ambience.Volume, value)) + return; + + ambience.Volume = value; Dirty(ambience); } private void HandleCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentHandleState args) { if (args.Current is not AmbientSoundComponentState state) return; - component.Enabled = state.Enabled; - component.Range = state.Range; - component.Volume = state.Volume; + SetAmbience(uid, state.Enabled, component); + SetRange(uid, state.Range, component); + SetVolume(uid, state.Volume, component); } private void GetCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentGetState args)