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