Add ambient music (#16829)

This commit is contained in:
metalgearsloth
2023-05-29 10:44:11 +10:00
committed by GitHub
parent f35fcff23f
commit 0c83642c5a
84 changed files with 1252 additions and 338 deletions

View File

@@ -0,0 +1,39 @@
using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Audio;
/// <summary>
/// Attaches a rules prototype to sound files to play ambience.
/// </summary>
[Prototype("ambientMusic")]
public sealed class AmbientMusicPrototype : IPrototype
{
[IdDataField] public string ID { get; } = string.Empty;
/// <summary>
/// Traditionally you'd prioritise most rules to least as priority but in our case we'll just be explicit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("priority")]
public int Priority = 0;
/// <summary>
/// Can we interrupt this ambience for a better prototype if possible?
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("interruptable")]
public bool Interruptable = false;
/// <summary>
/// Do we fade-in. Useful for songs.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("fadeIn")]
public bool FadeIn;
[ViewVariables(VVAccess.ReadWrite), DataField("sound", required: true)]
public SoundSpecifier Sound = default!;
[ViewVariables(VVAccess.ReadWrite), DataField("rules", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<RulesPrototype>))]
public string Rules = string.Empty;
}

View File

@@ -4,48 +4,47 @@ using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Serialization;
namespace Content.Shared.Audio
namespace Content.Shared.Audio;
[RegisterComponent]
[NetworkedComponent]
[Access(typeof(SharedAmbientSoundSystem))]
public sealed class AmbientSoundComponent : Component, IComponentTreeEntry<AmbientSoundComponent>
{
[RegisterComponent]
[NetworkedComponent]
[Access(typeof(SharedAmbientSoundSystem))]
public sealed class AmbientSoundComponent : Component, IComponentTreeEntry<AmbientSoundComponent>
{
[DataField("enabled")]
[ViewVariables(VVAccess.ReadWrite)] // only for map editing
public bool Enabled { get; set; } = true;
[DataField("enabled")]
[ViewVariables(VVAccess.ReadWrite)] // only for map editing
public bool Enabled { get; set; } = true;
[DataField("sound", required: true), ViewVariables(VVAccess.ReadWrite)] // only for map editing
public SoundSpecifier Sound = default!;
[DataField("sound", required: true), ViewVariables(VVAccess.ReadWrite)] // only for map editing
public SoundSpecifier Sound = default!;
/// <summary>
/// How far away this ambient sound can potentially be heard.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] // only for map editing
[DataField("range")]
public float Range = 2f;
/// <summary>
/// How far away this ambient sound can potentially be heard.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] // only for map editing
[DataField("range")]
public float Range = 2f;
/// <summary>
/// Applies this volume to the sound being played.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] // only for map editing
[DataField("volume")]
public float Volume = -10f;
/// <summary>
/// Applies this volume to the sound being played.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] // only for map editing
[DataField("volume")]
public float Volume = -10f;
public EntityUid? TreeUid { get; set; }
public EntityUid? TreeUid { get; set; }
public DynamicTree<ComponentTreeEntry<AmbientSoundComponent>>? Tree { get; set; }
public DynamicTree<ComponentTreeEntry<AmbientSoundComponent>>? Tree { get; set; }
public bool AddToTree => Enabled;
public bool AddToTree => Enabled;
public bool TreeUpdateQueued { get; set; }
}
[Serializable, NetSerializable]
public sealed class AmbientSoundComponentState : ComponentState
{
public bool Enabled { get; init; }
public float Range { get; init; }
public float Volume { get; init; }
}
public bool TreeUpdateQueued { get; set; }
}
[Serializable, NetSerializable]
public sealed class AmbientSoundComponentState : ComponentState
{
public bool Enabled { get; init; }
public float Range { get; init; }
public float Volume { get; init; }
}

View File

@@ -33,10 +33,6 @@ namespace Content.Shared.CCVar
/*
* Ambience
*/
//TODO: This is so that this compiles, yell at me if this is still in
public static readonly CVarDef<bool> AmbienceBasicEnabled =
CVarDef.Create("ambiance.basic_enabled", true, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// How long we'll wait until re-sampling nearby objects for ambience. Should be pretty fast, but doesn't have to match the tick rate.
@@ -74,24 +70,25 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<float> AmbienceVolume =
CVarDef.Create("ambience.volume", 0.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
// Midi is on engine so deal
public const float MidiMultiplier = 3f;
public const float AmbienceMultiplier = 2f;
/// <summary>
/// Ambience music volume.
/// </summary>
public static readonly CVarDef<float> AmbientMusicVolume =
CVarDef.Create("ambience.music_volume", 0.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
public const float AmbientMusicMultiplier = 2f;
/// <summary>
/// Lobby / round end music volume.
/// </summary>
public static readonly CVarDef<float> LobbyMusicVolume =
CVarDef.Create("ambience.lobby_music_volume", 0.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Whether to play the station ambience (humming) sound
/// </summary>
public static readonly CVarDef<bool> StationAmbienceEnabled =
CVarDef.Create("ambience.station_ambience", true, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Whether to play the space ambience
/// </summary>
public static readonly CVarDef<bool> SpaceAmbienceEnabled =
CVarDef.Create("ambience.space_ambience", true, CVar.ARCHIVE | CVar.CLIENTONLY);
/*
* Status
*/

View File

@@ -0,0 +1,27 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared.Morgue.Components;
[RegisterComponent, NetworkedComponent]
public sealed class MorgueComponent : Component
{
/// <summary>
/// Whether or not the morgue beeps if a living player is inside.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("doSoulBeep")]
public bool DoSoulBeep = true;
[ViewVariables]
public float AccumulatedFrameTime = 0f;
/// <summary>
/// The amount of time between each beep.
/// </summary>
[ViewVariables]
public float BeepTime = 10f;
[DataField("occupantHasSoulAlarmSound")]
public SoundSpecifier OccupantHasSoulAlarmSound = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
}

View File

@@ -253,10 +253,10 @@ namespace Content.Shared.Movement.Systems
if (!weightless && mobMoverQuery.TryGetComponent(uid, out var mobMover) &&
TryGetSound(weightless, uid, mover, mobMover, xform, out var sound))
{
var soundModifier = mover.Sprinting ? 1.5f : 1f;
var soundModifier = mover.Sprinting ? 3.5f : 1.5f;
var audioParams = sound.Params
.WithVolume(sound.Params.Volume * soundModifier)
.WithVolume(sound.Params.Volume + soundModifier)
.WithVariation(sound.Params.Variation ?? FootstepVariation);
// If we're a relay target then predict the sound for all relays.

View File

@@ -0,0 +1,46 @@
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Shared.Prayer;
/// <summary>
/// Allows an entity to be prayed on in the context menu
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class PrayableComponent : Component
{
/// <summary>
/// If bible users are only allowed to use this prayable entity
/// </summary>
[DataField("bibleUserOnly")]
[ViewVariables(VVAccess.ReadWrite)]
public bool BibleUserOnly;
/// <summary>
/// Message given to user to notify them a message was sent
/// </summary>
[DataField("sentMessage")]
[ViewVariables(VVAccess.ReadWrite)]
public string SentMessage = "prayer-popup-notify-pray-sent";
/// <summary>
/// Prefix used in the notification to admins
/// </summary>
[DataField("notifiactionPrefix")]
[ViewVariables(VVAccess.ReadWrite)]
public string NotificationPrefix = "prayer-chat-notify-pray";
/// <summary>
/// Used in window title and context menu
/// </summary>
[DataField("verb")]
[ViewVariables(VVAccess.ReadOnly)]
public string Verb = "prayer-verbs-pray";
/// <summary>
/// Context menu image
/// </summary>
[DataField("verbImage")]
[ViewVariables(VVAccess.ReadOnly)]
public SpriteSpecifier? VerbImage = new SpriteSpecifier.Texture(new ("/Textures/Interface/pray.svg.png"));
}

View File

@@ -0,0 +1,130 @@
using Content.Shared.Access;
using Content.Shared.Maps;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Random;
/// <summary>
/// Rules-based item selection. Can be used for any sort of conditional selection
/// Every single condition needs to be true for this to be selected.
/// e.g. "choose maintenance audio if 90% of tiles nearby are maintenance tiles"
/// </summary>
[Prototype("rules")]
public sealed class RulesPrototype : IPrototype
{
[IdDataField] public string ID { get; } = string.Empty;
[DataField("rules", required: true)]
public List<RulesRule> Rules = new();
}
[ImplicitDataDefinitionForInheritors]
public abstract class RulesRule
{
}
/// <summary>
/// Returns true if the attached entity is in space.
/// </summary>
public sealed class InSpaceRule : RulesRule
{
}
/// <summary>
/// Checks for entities matching the whitelist in range.
/// This is more expensive than <see cref="NearbyComponentsRule"/> so prefer that!
/// </summary>
public sealed class NearbyEntitiesRule : RulesRule
{
/// <summary>
/// How many of the entity need to be nearby.
/// </summary>
[DataField("count")]
public int Count = 1;
[DataField("whitelist", required: true)]
public EntityWhitelist Whitelist = new();
[DataField("range")]
public float Range = 10f;
}
public sealed class NearbyTilesPercentRule : RulesRule
{
[DataField("percent", required: true)]
public float Percent;
[DataField("tiles", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
public List<string> Tiles = new();
[DataField("range")]
public float Range = 10f;
}
/// <summary>
/// Always returns true. Used for fallbacks.
/// </summary>
public sealed class AlwaysTrueRule : RulesRule
{
}
/// <summary>
/// Returns true if on a grid or in range of one.
/// </summary>
public sealed class GridInRangeRule : RulesRule
{
[DataField("range")]
public float Range = 10f;
[DataField("inverted")]
public bool Inverted = false;
}
/// <summary>
/// Returns true if griduid and mapuid match (AKA on 'planet').
/// </summary>
public sealed class OnMapGridRule : RulesRule
{
}
/// <summary>
/// Checks for an entity nearby with the specified access.
/// </summary>
public sealed class NearbyAccessRule : RulesRule
{
// This exists because of doorelectronics contained inside doors.
/// <summary>
/// Does the access entity need to be anchored.
/// </summary>
[DataField("anchored")]
public bool Anchored = true;
/// <summary>
/// Count of entities that need to be nearby.
/// </summary>
[DataField("count")]
public int Count = 1;
[DataField("access", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<AccessLevelPrototype>))]
public List<string> Access = new();
[DataField("range")]
public float Range = 10f;
}
public sealed class NearbyComponentsRule : RulesRule
{
[DataField("count")] public int Count;
[DataField("components", required: true)]
public ComponentRegistry Components = default!;
[DataField("range")]
public float Range = 10f;
}

View File

@@ -0,0 +1,207 @@
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Shared.Random;
public sealed class RulesSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
[Dependency] private readonly AccessReaderSystem _reader = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public bool IsTrue(EntityUid uid, RulesPrototype rules)
{
foreach (var rule in rules.Rules)
{
switch (rule)
{
case AlwaysTrueRule:
break;
case GridInRangeRule griddy:
{
if (!TryComp<TransformComponent>(uid, out var xform))
{
return false;
}
if (xform.GridUid != null)
{
return !griddy.Inverted;
}
var worldPos = _transform.GetWorldPosition(xform);
foreach (var _ in _mapManager.FindGridsIntersecting(
xform.MapID,
new Box2(worldPos - griddy.Range, worldPos + griddy.Range)))
{
return !griddy.Inverted;
}
break;
}
case InSpaceRule:
{
if (!TryComp<TransformComponent>(uid, out var xform) ||
xform.GridUid != null)
{
return false;
}
break;
}
case NearbyAccessRule access:
{
var xformQuery = GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(uid, out var xform) ||
xform.MapUid == null)
{
return false;
}
var found = false;
var worldPos = _transform.GetWorldPosition(xform, xformQuery);
var count = 0;
// TODO: Update this when we get the callback version
foreach (var comp in _lookup.GetComponentsInRange<AccessReaderComponent>(xform.MapID,
worldPos, access.Range))
{
if (access.Anchored && !xformQuery.GetComponent(comp.Owner).Anchored ||
!_reader.AreAccessTagsAllowed(access.Access, comp))
{
continue;
}
count++;
if (count < access.Count)
continue;
found = true;
break;
}
if (!found)
return false;
break;
}
case NearbyComponentsRule nearbyComps:
{
if (!TryComp<TransformComponent>(uid, out var xform) ||
xform.MapUid == null)
{
return false;
}
var found = false;
var worldPos = _transform.GetWorldPosition(xform);
var count = 0;
foreach (var comp in nearbyComps.Components.Values)
{
// TODO: Update this when we get the callback version
foreach (var _ in _lookup.GetComponentsInRange(comp.Component.GetType(), xform.MapID,
worldPos, nearbyComps.Range))
{
count++;
if (count >= nearbyComps.Count)
{
found = true;
break;
}
}
if (found)
break;
}
if (!found)
return false;
break;
}
case NearbyEntitiesRule entity:
{
if (!TryComp<TransformComponent>(uid, out var xform) ||
xform.MapUid == null)
{
return false;
}
var found = false;
var worldPos = _transform.GetWorldPosition(xform);
var count = 0;
foreach (var ent in _lookup.GetEntitiesInRange(xform.MapID, worldPos, entity.Range))
{
if (!entity.Whitelist.IsValid(ent, EntityManager))
continue;
count++;
if (count < entity.Count)
continue;
found = true;
break;
}
if (!found)
return false;
break;
}
case NearbyTilesPercentRule tiles:
{
if (!TryComp<TransformComponent>(uid, out var xform) ||
!TryComp<MapGridComponent>(xform.GridUid, out var grid))
{
return false;
}
var tileCount = 0;
var matchingTileCount = 0;
foreach (var tile in grid.GetTilesIntersecting(new Circle(_transform.GetWorldPosition(xform),
tiles.Range)))
{
tileCount++;
if (!tiles.Tiles.Contains(_tileDef[tile.Tile.TypeId].ID))
continue;
matchingTileCount++;
}
if (matchingTileCount / (float) tileCount < tiles.Percent)
return false;
break;
}
case OnMapGridRule:
{
if (!TryComp<TransformComponent>(uid, out var xform) ||
xform.GridUid != xform.MapUid ||
xform.MapUid == null)
{
return false;
}
break;
}
default:
throw new NotImplementedException();
}
}
return true;
}
}