diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs
new file mode 100644
index 0000000000..ce6e781e3f
--- /dev/null
+++ b/Content.Client/Audio/AmbientSoundSystem.cs
@@ -0,0 +1,178 @@
+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;
+
+namespace Content.Client.Audio
+{
+ ///
+ /// Samples nearby and plays audio.
+ ///
+ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
+ {
+ [Dependency] private IEntityLookup _lookup = 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 (_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 = player.Transform.Coordinates;
+
+ foreach (var (comp, (stream, _)) in _playingSounds.ToArray())
+ {
+ if (!comp.Deleted && comp.Enabled && comp.Owner.Transform.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 (!entity.TryGetComponent(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.
+ !entity.Transform.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);
+ }
+ }
+ }
+}
diff --git a/Content.Client/Audio/BackgroundAudioSystem.cs b/Content.Client/Audio/BackgroundAudioSystem.cs
index 8a6f2b2607..3b5480e0f7 100644
--- a/Content.Client/Audio/BackgroundAudioSystem.cs
+++ b/Content.Client/Audio/BackgroundAudioSystem.cs
@@ -29,8 +29,8 @@ namespace Content.Client.Audio
private SoundCollectionPrototype _ambientCollection = default!;
- private AudioParams _ambientParams = new(-10f, 1, "Master", 0, 0, true, 0f);
- private AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, true, 0f);
+ private AudioParams _ambientParams = new(-10f, 1, "Master", 0, 0, 0, true, 0f);
+ private AudioParams _lobbyParams = new(-5f, 1, "Master", 0, 0, 0, true, 0f);
private IPlayingAudioStream? _ambientStream;
private IPlayingAudioStream? _lobbyStream;
diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index ae17f9ae33..43e3feb619 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -272,7 +272,8 @@ namespace Content.Client.Entry
"Advertise",
"PowerNetworkBattery",
"BatteryCharger",
- "SpawnItemsOnUse"
+ "SpawnItemsOnUse",
+ "AmbientOnPowered",
};
}
}
diff --git a/Content.Server/Audio/AmbientOnPoweredComponent.cs b/Content.Server/Audio/AmbientOnPoweredComponent.cs
new file mode 100644
index 0000000000..1ffb2d0349
--- /dev/null
+++ b/Content.Server/Audio/AmbientOnPoweredComponent.cs
@@ -0,0 +1,14 @@
+using Content.Shared.Audio;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Audio
+{
+ ///
+ /// Toggles on when powered and off when not powered.
+ ///
+ [RegisterComponent]
+ public class AmbientOnPoweredComponent : Component
+ {
+ public override string Name => "AmbientOnPowered";
+ }
+}
diff --git a/Content.Server/Audio/AmbientSoundSystem.cs b/Content.Server/Audio/AmbientSoundSystem.cs
new file mode 100644
index 0000000000..c61be5164c
--- /dev/null
+++ b/Content.Server/Audio/AmbientSoundSystem.cs
@@ -0,0 +1,23 @@
+using Content.Server.Power.Components;
+using Content.Shared.Audio;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Audio
+{
+ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(HandlePowerChange);
+ }
+
+ private void HandlePowerChange(EntityUid uid, AmbientOnPoweredComponent component, PowerChangedEvent args)
+ {
+ if (!ComponentManager.TryGetComponent(uid, out var ambientSound)) return;
+ if (ambientSound.Enabled == args.Powered) return;
+ ambientSound.Enabled = args.Powered;
+ ambientSound.Dirty();
+ }
+ }
+}
diff --git a/Content.Server/Gravity/GravityGeneratorComponent.cs b/Content.Server/Gravity/GravityGeneratorComponent.cs
index 15605cd785..f56085b5fc 100644
--- a/Content.Server/Gravity/GravityGeneratorComponent.cs
+++ b/Content.Server/Gravity/GravityGeneratorComponent.cs
@@ -1,6 +1,7 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.Acts;
+using Content.Shared.Audio;
using Content.Shared.Gravity;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
@@ -137,6 +138,7 @@ namespace Content.Server.Gravity
private void MakeBroken()
{
_status = GravityGeneratorStatus.Broken;
+ EntitySystem.Get().SetAmbience(Owner.Uid, false);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
@@ -145,6 +147,7 @@ namespace Content.Server.Gravity
private void MakeUnpowered()
{
_status = GravityGeneratorStatus.Unpowered;
+ EntitySystem.Get().SetAmbience(Owner.Uid, false);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
@@ -153,6 +156,7 @@ namespace Content.Server.Gravity
private void MakeOff()
{
_status = GravityGeneratorStatus.Off;
+ EntitySystem.Get().SetAmbience(Owner.Uid, false);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, false);
@@ -161,6 +165,7 @@ namespace Content.Server.Gravity
private void MakeOn()
{
_status = GravityGeneratorStatus.On;
+ EntitySystem.Get().SetAmbience(Owner.Uid, true);
_appearance?.SetData(GravityGeneratorVisuals.State, Status);
_appearance?.SetData(GravityGeneratorVisuals.CoreVisible, true);
diff --git a/Content.Server/Light/Components/PoweredLightComponent.cs b/Content.Server/Light/Components/PoweredLightComponent.cs
index f5fc932908..d5d830bd01 100644
--- a/Content.Server/Light/Components/PoweredLightComponent.cs
+++ b/Content.Server/Light/Components/PoweredLightComponent.cs
@@ -4,6 +4,7 @@ using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
+using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
@@ -226,7 +227,7 @@ namespace Content.Server.Light.Components
if (LightBulb == null) // No light bulb.
{
- _currentLit = false;
+ SetLight(false);
powerReceiver.Load = 0;
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Empty);
return;
@@ -267,6 +268,7 @@ namespace Content.Server.Light.Components
private void SetLight(bool value, Color? color = null)
{
_currentLit = value;
+ EntitySystem.Get().SetAmbience(Owner.Uid, value);
if (!Owner.TryGetComponent(out PointLightComponent? pointLight)) return;
pointLight.Enabled = value;
@@ -327,6 +329,6 @@ namespace Content.Server.Light.Components
UpdateLight();
}
-
+
}
}
diff --git a/Content.Shared/Audio/AmbientSoundComponent.cs b/Content.Shared/Audio/AmbientSoundComponent.cs
new file mode 100644
index 0000000000..6f795634d8
--- /dev/null
+++ b/Content.Shared/Audio/AmbientSoundComponent.cs
@@ -0,0 +1,46 @@
+using System;
+using Content.Shared.Sound;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Shared.Audio
+{
+ [RegisterComponent]
+ [NetworkedComponent]
+ public sealed class AmbientSoundComponent : Component
+ {
+ public override string Name => "AmbientSound";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("enabled")]
+ public bool Enabled { get; set; } = true;
+
+ [DataField("sound")]
+ public SoundSpecifier Sound = default!;
+
+ ///
+ /// How far away this ambient sound can potentially be heard.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("range")]
+ public float Range = 2f;
+
+ ///
+ /// Applies this volume to the sound being played.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("volume")]
+ public float Volume = -10f;
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class AmbientSoundComponentState : ComponentState
+ {
+ public bool Enabled { get; init; }
+ public float Range { get; init; }
+ public float Volume { get; init; }
+ }
+}
diff --git a/Content.Shared/Audio/SharedAmbientSoundSystem.cs b/Content.Shared/Audio/SharedAmbientSoundSystem.cs
new file mode 100644
index 0000000000..f5500e69e5
--- /dev/null
+++ b/Content.Shared/Audio/SharedAmbientSoundSystem.cs
@@ -0,0 +1,45 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Audio
+{
+ public abstract class SharedAmbientSoundSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(GetCompState);
+ SubscribeLocalEvent(HandleCompState);
+ }
+
+ public void SetAmbience(EntityUid uid, bool value)
+ {
+ // 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 (!ComponentManager.TryGetComponent(uid, out var ambience) ||
+ ambience.Enabled == value) return;
+
+ ambience.Enabled = value;
+ ambience.Dirty();
+ }
+
+ 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;
+ }
+
+ private void GetCompState(EntityUid uid, AmbientSoundComponent component, ref ComponentGetState args)
+ {
+ args.State = new AmbientSoundComponentState
+ {
+ Enabled = component.Enabled,
+ Range = component.Range,
+ Volume = component.Volume,
+ };
+ }
+ }
+}
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index 7491e4c63b..8f19ee2b0b 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -7,6 +7,22 @@ namespace Content.Shared.CCVar
[CVarDefs]
public sealed class CCVars : CVars
{
+ /*
+ * Ambience
+ */
+
+ ///
+ /// How long we'll wait until re-sampling nearby objects for ambience.
+ ///
+ public static readonly CVarDef AmbientCooldown =
+ CVarDef.Create("ambience.cooldown", 0.5f, CVar.REPLICATED | CVar.SERVER);
+
+ public static readonly CVarDef AmbientRange =
+ CVarDef.Create("ambience.range", 5f, CVar.REPLICATED | CVar.SERVER);
+
+ public static readonly CVarDef MaxAmbientSources =
+ CVarDef.Create("ambience.max_sounds", 6, CVar.REPLICATED | CVar.SERVER);
+
/*
* Status
*/
diff --git a/Content.Shared/Light/Component/SharedExpendableLightComponent.cs b/Content.Shared/Light/Component/SharedExpendableLightComponent.cs
index 48dd3b154d..5d8b6dca52 100644
--- a/Content.Shared/Light/Component/SharedExpendableLightComponent.cs
+++ b/Content.Shared/Light/Component/SharedExpendableLightComponent.cs
@@ -29,7 +29,7 @@ namespace Content.Shared.Light.Component
[NetworkedComponent]
public abstract class SharedExpendableLightComponent: Robust.Shared.GameObjects.Component
{
- public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, true, 0.3f);
+ public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, 1, true, 0.3f);
public sealed override string Name => "ExpendableLight";
diff --git a/Resources/Audio/Ambience/Objects/buzzing.ogg b/Resources/Audio/Ambience/Objects/buzzing.ogg
new file mode 100644
index 0000000000..6c1b451cc0
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/buzzing.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/emf_buzz.ogg b/Resources/Audio/Ambience/Objects/emf_buzz.ogg
new file mode 100644
index 0000000000..a51c3c03ab
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/emf_buzz.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/engine_hum.ogg b/Resources/Audio/Ambience/Objects/engine_hum.ogg
new file mode 100644
index 0000000000..6f563810a7
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/engine_hum.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/gravity_gen_hum.ogg b/Resources/Audio/Ambience/Objects/gravity_gen_hum.ogg
new file mode 100644
index 0000000000..5b30bab463
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/gravity_gen_hum.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/hdd_buzz.ogg b/Resources/Audio/Ambience/Objects/hdd_buzz.ogg
new file mode 100644
index 0000000000..aaf52a4b1e
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/hdd_buzz.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/light_hum.ogg b/Resources/Audio/Ambience/Objects/light_hum.ogg
new file mode 100644
index 0000000000..4bc024c405
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/light_hum.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/periodic_beep.ogg b/Resources/Audio/Ambience/Objects/periodic_beep.ogg
new file mode 100644
index 0000000000..4b0b16e8dd
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/periodic_beep.ogg differ
diff --git a/Resources/Audio/Ambience/Objects/vending_machine_hum.ogg b/Resources/Audio/Ambience/Objects/vending_machine_hum.ogg
new file mode 100644
index 0000000000..5208cc63e8
Binary files /dev/null and b/Resources/Audio/Ambience/Objects/vending_machine_hum.ogg differ
diff --git a/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml b/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml
index f06d0cdae4..f79a874044 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml
@@ -6,6 +6,10 @@
placement:
mode: AlignTileAny
components:
+ - type: AmbientSound
+ range: 7
+ sound:
+ path: /Audio/Ambience/Objects/gravity_gen_hum.ogg
- type: Sprite
netsync: false
sprite: Structures/Machines/gravity_generator.rsi
diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
index 5c26aa4ab6..aed5fc1c89 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
@@ -5,6 +5,12 @@
description: Just add capitalism!
abstract: true
components:
+ - type: AmbientOnPowered
+ - type: AmbientSound
+ volume: -15
+ range: 3
+ sound:
+ path: /Audio/Ambience/Objects/vending_machine_hum.ogg
- type: Sprite
sprite: Structures/Machines/VendingMachines/empty.rsi
netsync: false
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/generator.yml
index 2395571387..d4c583814e 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/generator.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/generator.yml
@@ -6,6 +6,10 @@
placement:
mode: SnapgridCenter
components:
+ #- type: AmbientSound
+ # range: 5
+ # sound:
+ # path: /Audio/Ambience/Objects/engine_hum.ogg
- type: Clickable
- type: InteractionOutline
- type: Physics
diff --git a/Resources/Prototypes/Entities/Structures/Power/parts.yml b/Resources/Prototypes/Entities/Structures/Power/parts.yml
index 80d765325d..ae71a1fba0 100644
--- a/Resources/Prototypes/Entities/Structures/Power/parts.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/parts.yml
@@ -46,6 +46,9 @@
placement:
mode: SnapgridCenter
components:
+ - type: AmbientSound
+ sound:
+ path: /Audio/Ambience/Objects/hdd_buzz.ogg
- type: Clickable
- type: AccessReader
access: [["Engineering"]]
diff --git a/Resources/Prototypes/Entities/Structures/Power/smes.yml b/Resources/Prototypes/Entities/Structures/Power/smes.yml
index 6c3796f1ef..9a8b039ed5 100644
--- a/Resources/Prototypes/Entities/Structures/Power/smes.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/smes.yml
@@ -7,6 +7,10 @@
placement:
mode: SnapgridCenter
components:
+ - type: AmbientSound
+ range: 3
+ sound:
+ path: /Audio/Ambience/Objects/periodic_beep.ogg
- type: Sprite
netsync: false
sprite: Structures/Power/smes.rsi
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml
index 36ed449da4..26c8481c56 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml
@@ -4,6 +4,10 @@
description: "An unpowered light."
suffix: Unpowered
components:
+ - type: AmbientSound
+ volume: -12
+ sound:
+ path: /Audio/Ambience/Objects/light_hum.ogg
- type: Clickable
- type: InteractionOutline
- type: Construction