* Use new Subs.CVar helper Removes manual config OnValueChanged calls, removes need to remember to manually unsubscribe. This both reduces boilerplate and fixes many issues where subscriptions weren't removed on entity system shutdown. * Fix a bunch of warnings * More warning fixes * Use new DateTime serializer to get rid of ISerializationHooks in changelog code. * Get rid of some more ISerializationHooks for enums * And a little more * Apply suggestions from code review Co-authored-by: 0x6273 <0x40@keemail.me> --------- Co-authored-by: 0x6273 <0x40@keemail.me>
171 lines
6.0 KiB
C#
171 lines
6.0 KiB
C#
using System.Numerics;
|
|
using Content.Shared.Weather;
|
|
using Robust.Client.Audio;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.Player;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.GameStates;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Physics.Systems;
|
|
using Robust.Shared.Player;
|
|
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
|
|
|
|
namespace Content.Client.Weather;
|
|
|
|
public sealed class WeatherSystem : SharedWeatherSystem
|
|
{
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
[Dependency] private readonly AudioSystem _audio = default!;
|
|
[Dependency] private readonly MapSystem _mapSystem = default!;
|
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
SubscribeLocalEvent<WeatherComponent, ComponentHandleState>(OnWeatherHandleState);
|
|
}
|
|
|
|
protected override void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime)
|
|
{
|
|
base.Run(uid, weather, weatherProto, frameTime);
|
|
|
|
var ent = _playerManager.LocalEntity;
|
|
|
|
if (ent == null)
|
|
return;
|
|
|
|
var mapUid = Transform(uid).MapUid;
|
|
var entXform = Transform(ent.Value);
|
|
|
|
// Maybe have the viewports manage this?
|
|
if (mapUid == null || entXform.MapUid != mapUid)
|
|
{
|
|
weather.Stream = _audio.Stop(weather.Stream);
|
|
return;
|
|
}
|
|
|
|
if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null)
|
|
return;
|
|
|
|
weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true).Value.Entity;
|
|
|
|
var stream = weather.Stream.Value;
|
|
var comp = Comp<AudioComponent>(stream);
|
|
var occlusion = 0f;
|
|
|
|
// Work out tiles nearby to determine volume.
|
|
if (TryComp<MapGridComponent>(entXform.GridUid, out var grid))
|
|
{
|
|
var gridId = entXform.GridUid.Value;
|
|
// Floodfill to the nearest tile and use that for audio.
|
|
var seed = _mapSystem.GetTileRef(gridId, grid, entXform.Coordinates);
|
|
var frontier = new Queue<TileRef>();
|
|
frontier.Enqueue(seed);
|
|
// If we don't have a nearest node don't play any sound.
|
|
EntityCoordinates? nearestNode = null;
|
|
var visited = new HashSet<Vector2i>();
|
|
|
|
while (frontier.TryDequeue(out var node))
|
|
{
|
|
if (!visited.Add(node.GridIndices))
|
|
continue;
|
|
|
|
if (!CanWeatherAffect(grid, node))
|
|
{
|
|
// Add neighbors
|
|
// TODO: Ideally we pick some deterministically random direction and use that
|
|
// We can't just do that naively here because it will flicker between nearby tiles.
|
|
for (var x = -1; x <= 1; x++)
|
|
{
|
|
for (var y = -1; y <= 1; y++)
|
|
{
|
|
if (Math.Abs(x) == 1 && Math.Abs(y) == 1 ||
|
|
x == 0 && y == 0 ||
|
|
(new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length() > 3)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
frontier.Enqueue(_mapSystem.GetTileRef(gridId, grid, new Vector2i(x, y) + node.GridIndices));
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
nearestNode = new EntityCoordinates(entXform.GridUid.Value,
|
|
node.GridIndices + grid.TileSizeHalfVector);
|
|
break;
|
|
}
|
|
|
|
// Get occlusion to the targeted node if it exists, otherwise set a default occlusion.
|
|
if (nearestNode != null)
|
|
{
|
|
var entPos = _transform.GetMapCoordinates(entXform);
|
|
var nodePosition = nearestNode.Value.ToMap(EntityManager, _transform).Position;
|
|
var delta = nodePosition - entPos.Position;
|
|
var distance = delta.Length();
|
|
occlusion = _audio.GetOcclusion(entPos, delta, distance);
|
|
}
|
|
else
|
|
{
|
|
occlusion = 3f;
|
|
}
|
|
}
|
|
|
|
var alpha = GetPercent(weather, uid);
|
|
alpha *= SharedAudioSystem.VolumeToGain(weatherProto.Sound.Params.Volume);
|
|
_audio.SetGain(stream, alpha, comp);
|
|
comp.Occlusion = occlusion;
|
|
}
|
|
|
|
protected override bool SetState(WeatherState state, WeatherComponent comp, WeatherData weather, WeatherPrototype weatherProto)
|
|
{
|
|
if (!base.SetState(state, comp, weather, weatherProto))
|
|
return false;
|
|
|
|
if (!Timing.IsFirstTimePredicted)
|
|
return true;
|
|
|
|
// TODO: Fades (properly)
|
|
weather.Stream = _audio.Stop(weather.Stream);
|
|
weather.Stream = _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true)?.Entity;
|
|
return true;
|
|
}
|
|
|
|
private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args)
|
|
{
|
|
if (args.Current is not WeatherComponentState state)
|
|
return;
|
|
|
|
foreach (var (proto, weather) in component.Weather)
|
|
{
|
|
// End existing one
|
|
if (!state.Weather.TryGetValue(proto, out var stateData))
|
|
{
|
|
EndWeather(uid, component, proto);
|
|
continue;
|
|
}
|
|
|
|
// Data update?
|
|
weather.StartTime = stateData.StartTime;
|
|
weather.EndTime = stateData.EndTime;
|
|
weather.State = stateData.State;
|
|
}
|
|
|
|
foreach (var (proto, weather) in state.Weather)
|
|
{
|
|
if (component.Weather.ContainsKey(proto))
|
|
continue;
|
|
|
|
// New weather
|
|
StartWeather(component, ProtoMan.Index<WeatherPrototype>(proto), weather.EndTime);
|
|
}
|
|
}
|
|
}
|