return false;
if (!_entManager.TryGetComponent<WeatherComponent>(_mapManager.GetMapEntityId(args.MapId), out var weather) ||
- weather.Weather == null)
+ weather.Weather.Count == 0)
{
return false;
}
{
var mapUid = _mapManager.GetMapEntityId(args.MapId);
- if (!_entManager.TryGetComponent<WeatherComponent>(mapUid, out var weather) ||
- weather.Weather == null ||
- !_protoManager.TryIndex<WeatherPrototype>(weather.Weather, out var weatherProto))
+ if (!_entManager.TryGetComponent<WeatherComponent>(mapUid, out var comp))
{
return;
}
- var alpha = _weather.GetPercent(weather, mapUid, weatherProto);
- DrawWorld(args, weatherProto, alpha);
+ foreach (var (proto, weather) in comp.Weather)
+ {
+ if (!_protoManager.TryIndex<WeatherPrototype>(proto, out var weatherProto))
+ continue;
+
+ var alpha = _weather.GetPercent(weather, mapUid);
+ DrawWorld(args, weatherProto, alpha);
+ }
}
private void DrawWorld(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha)
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly AudioSystem _audio = default!;
- [Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
- // Consistency isn't really important, just want to avoid sharp changes and there's no way to lerp on engine nicely atm.
- private float _lastAlpha;
- private float _lastOcclusion;
-
private const float OcclusionLerpRate = 4f;
private const float AlphaLerpRate = 4f;
_overlayManager.RemoveOverlay<WeatherOverlay>();
}
- protected override void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime)
+ protected override void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime)
{
- base.Run(uid, component, weather, state, frameTime);
+ base.Run(uid, weather, weatherProto, frameTime);
var ent = _playerManager.LocalPlayer?.ControlledEntity;
// Maybe have the viewports manage this?
if (mapUid == null || entXform.MapUid != mapUid)
{
- _lastOcclusion = 0f;
- _lastAlpha = 0f;
- component.Stream?.Stop();
- component.Stream = null;
+ weather.LastOcclusion = 0f;
+ weather.LastAlpha = 0f;
+ weather.Stream?.Stop();
+ weather.Stream = null;
return;
}
- if (!Timing.IsFirstTimePredicted || weather.Sound == null)
+ if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null)
return;
- component.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true);
- var volumeMod = MathF.Pow(10, weather.Sound.Params.Volume / 10f);
+ weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true);
+ var volumeMod = MathF.Pow(10, weatherProto.Sound.Params.Volume / 10f);
- var stream = (AudioSystem.PlayingStream) component.Stream!;
- var alpha = GetPercent(component, mapUid.Value, weather);
+ var stream = (AudioSystem.PlayingStream) weather.Stream!;
+ var alpha = weather.LastAlpha;
alpha = MathF.Pow(alpha, 2f) * volumeMod;
// TODO: Lerp this occlusion.
var occlusion = 0f;
}
}
- if (MathHelper.CloseTo(_lastOcclusion, occlusion, 0.01f))
- _lastOcclusion = occlusion;
+ if (MathHelper.CloseTo(weather.LastOcclusion, occlusion, 0.01f))
+ weather.LastOcclusion = occlusion;
else
- _lastOcclusion += (occlusion - _lastOcclusion) * OcclusionLerpRate * frameTime;
+ weather.LastOcclusion += (occlusion - weather.LastOcclusion) * OcclusionLerpRate * frameTime;
- if (MathHelper.CloseTo(_lastAlpha, alpha, 0.01f))
- _lastAlpha = alpha;
+ if (MathHelper.CloseTo(weather.LastAlpha, alpha, 0.01f))
+ weather.LastAlpha = alpha;
else
- _lastAlpha += (alpha - _lastAlpha) * AlphaLerpRate * frameTime;
+ weather.LastAlpha += (alpha - weather.LastAlpha) * AlphaLerpRate * frameTime;
// Full volume if not on grid
- stream.Source.SetVolumeDirect(_lastAlpha);
- stream.Source.SetOcclusion(_lastOcclusion);
+ stream.Source.SetVolumeDirect(weather.LastAlpha);
+ stream.Source.SetOcclusion(weather.LastOcclusion);
}
- public float GetPercent(WeatherComponent component, EntityUid mapUid, WeatherPrototype weatherProto)
+ protected override void EndWeather(EntityUid uid, WeatherComponent component, string proto)
{
- var pauseTime = _metadata.GetPauseTime(mapUid);
- var elapsed = Timing.CurTime - (component.StartTime + pauseTime);
- var duration = component.Duration;
- var remaining = duration - elapsed;
- float alpha;
+ base.EndWeather(uid, component, proto);
- if (elapsed < weatherProto.StartupTime)
- {
- alpha = (float) (elapsed / weatherProto.StartupTime);
- }
- else if (remaining < weatherProto.ShutdownTime)
- {
- alpha = (float) (remaining / weatherProto.ShutdownTime);
- }
- else
- {
- alpha = 1f;
- }
+ if (!component.Weather.TryGetValue(proto, out var weather))
+ return;
- return alpha;
+ weather.LastAlpha = 0f;
+ weather.LastOcclusion = 0f;
}
- protected override bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype)
+ protected override bool SetState(WeatherState state, WeatherComponent comp, WeatherData weather, WeatherPrototype weatherProto)
{
- if (!base.SetState(uid, component, state, prototype))
+ if (!base.SetState(state, comp, weather, weatherProto))
return false;
if (!Timing.IsFirstTimePredicted)
return true;
- // TODO: Fades
- component.Stream?.Stop();
- component.Stream = null;
- component.Stream = _audio.PlayGlobal(prototype.Sound, Filter.Local(), true);
+ // TODO: Fades (properly)
+ weather.Stream?.Stop();
+ weather.Stream = null;
+ weather.Stream = _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true);
return true;
}
- protected override void EndWeather(WeatherComponent component)
- {
- _lastOcclusion = 0f;
- _lastAlpha = 0f;
- base.EndWeather(component);
- }
-
private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args)
{
if (args.Current is not WeatherComponentState state)
return;
- if (component.Weather != state.Weather || !component.EndTime.Equals(state.EndTime) || !component.StartTime.Equals(state.StartTime))
+ foreach (var (proto, weather) in component.Weather)
{
- EndWeather(component);
+ // End existing one
+ if (!state.Weather.TryGetValue(proto, out var stateData))
+ {
+ EndWeather(uid, component, proto);
+ continue;
+ }
- if (state.Weather != null)
- StartWeather(component, ProtoMan.Index<WeatherPrototype>(state.Weather));
+ // Data update?
+ weather.StartTime = stateData.StartTime;
+ weather.EndTime = stateData.EndTime;
+ weather.State = stateData.State;
}
- component.EndTime = state.EndTime;
- component.StartTime = state.StartTime;
+ foreach (var (proto, weather) in state.Weather)
+ {
+ if (component.Weather.ContainsKey(proto))
+ continue;
+
+ // New weather
+ StartWeather(component, ProtoMan.Index<WeatherPrototype>(proto), weather.EndTime);
+ weather.LastAlpha = 0f;
+ }
}
}
private void OnWeatherGetState(EntityUid uid, WeatherComponent component, ref ComponentGetState args)
{
- args.State = new WeatherComponentState()
- {
- Weather = component.Weather,
- EndTime = component.EndTime,
- StartTime = component.StartTime,
- };
+ args.State = new WeatherComponentState(component.Weather);
}
[AdminCommand(AdminFlags.Fun)]
private void WeatherTwo(IConsoleShell shell, string argstr, string[] args)
{
- if (args.Length != 2)
+ if (args.Length < 2)
{
+ shell.WriteError($"A");
return;
}
return;
}
+ TimeSpan? endTime = null;
+
+ if (args.Length == 3)
+ {
+ if (int.TryParse(args[2], out var durationInt))
+ {
+ var curTime = Timing.CurTime;
+ var maxTime = TimeSpan.MaxValue;
+
+ // If it's already running then just fade out with how much time we're into the weather.
+ if (TryComp<WeatherComponent>(MapManager.GetMapEntityId(mapId), out var weatherComp) &&
+ weatherComp.Weather.TryGetValue(args[1], out var existing))
+ {
+ maxTime = curTime - existing.StartTime;
+ }
+
+ endTime = curTime + TimeSpan.FromSeconds(durationInt);
+
+ if (endTime > maxTime)
+ endTime = maxTime;
+ }
+ }
+
if (args[1].Equals("null"))
{
- SetWeather(mapId, null);
+ SetWeather(mapId, null, endTime);
}
else if (ProtoMan.TryIndex<WeatherPrototype>(args[1], out var weatherProto))
{
- SetWeather(mapId, weatherProto);
+ SetWeather(mapId, weatherProto, endTime);
}
else
{
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
-using Robust.Shared.Utility;
namespace Content.Shared.Weather;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
protected ISawmill Sawmill = default!;
private void OnWeatherUnpaused(EntityUid uid, WeatherComponent component, ref EntityUnpausedEvent args)
{
- component.EndTime += args.PausedTime;
+ foreach (var weather in component.Weather.Values)
+ {
+ weather.StartTime += args.PausedTime;
+
+ if (weather.EndTime != null)
+ weather.EndTime = weather.EndTime.Value + args.PausedTime;
+ }
}
public bool CanWeatherAffect(MapGridComponent grid, TileRef tileRef, EntityQuery<PhysicsComponent> bodyQuery)
}
+ public float GetPercent(WeatherData component, EntityUid mapUid)
+ {
+ var pauseTime = _metadata.GetPauseTime(mapUid);
+ var elapsed = Timing.CurTime - (component.StartTime + pauseTime);
+ var duration = component.Duration;
+ var remaining = duration - elapsed;
+ float alpha;
+
+ if (remaining < WeatherComponent.ShutdownTime)
+ {
+ alpha = (float) (remaining / WeatherComponent.ShutdownTime);
+ }
+ else if (elapsed < WeatherComponent.StartupTime)
+ {
+ alpha = (float) (elapsed / WeatherComponent.StartupTime);
+ }
+ else
+ {
+ alpha = 1f;
+ }
+
+ return alpha;
+ }
+
+
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = Timing.CurTime;
- foreach (var (comp, metadata) in EntityQuery<WeatherComponent, MetaDataComponent>())
+ foreach (var comp in EntityQuery<WeatherComponent>())
{
- if (comp.Weather == null)
+ if (comp.Weather.Count == 0)
continue;
var uid = comp.Owner;
- var endTime = comp.EndTime;
- // Ended
- if (endTime < curTime)
+ foreach (var (proto, weather) in comp.Weather)
{
- EndWeather(comp);
- continue;
- }
+ var endTime = weather.EndTime;
- // Admin messed up or the likes.
- if (!ProtoMan.TryIndex<WeatherPrototype>(comp.Weather, out var weatherProto))
- {
- Sawmill.Error($"Unable to find weather prototype for {comp.Weather}, ending!");
- EndWeather(comp);
- continue;
- }
+ // Ended
+ if (endTime != null && endTime < curTime)
+ {
+ EndWeather(uid, comp, proto);
+ continue;
+ }
- var remainingTime = endTime - curTime;
+ var remainingTime = endTime - curTime;
- // Shutting down
- if (remainingTime < weatherProto.ShutdownTime)
- {
- SetState(uid, comp, WeatherState.Ending, weatherProto);
- }
- // Starting up
- else
- {
- var startTime = comp.StartTime;
- var elapsed = Timing.CurTime - startTime;
+ // Admin messed up or the likes.
+ if (!ProtoMan.TryIndex<WeatherPrototype>(proto, out var weatherProto))
+ {
+ Sawmill.Error($"Unable to find weather prototype for {comp.Weather}, ending!");
+ EndWeather(uid, comp, proto);
+ continue;
+ }
- if (elapsed < weatherProto.StartupTime)
+ // Shutting down
+ if (endTime != null && remainingTime < WeatherComponent.ShutdownTime)
{
- SetState(uid, comp, WeatherState.Starting, weatherProto);
+ SetState(WeatherState.Ending, comp, weather, weatherProto);
+ }
+ // Starting up
+ else
+ {
+ var startTime = weather.StartTime;
+ var elapsed = Timing.CurTime - startTime;
+
+ if (elapsed < WeatherComponent.StartupTime)
+ {
+ SetState(WeatherState.Starting, comp, weather, weatherProto);
+ }
}
- }
- // Run whatever code we need.
- Run(uid, comp, weatherProto, comp.State, frameTime);
+ // Run whatever code we need.
+ Run(uid, weather, weatherProto, frameTime);
+ }
}
}
- public void SetWeather(MapId mapId, WeatherPrototype? weather)
+ /// <summary>
+ /// Shuts down all existing weather and starts the new one if applicable.
+ /// </summary>
+ public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime)
{
var weatherComp = EnsureComp<WeatherComponent>(MapManager.GetMapEntityId(mapId));
- EndWeather(weatherComp);
- if (weather != null)
- StartWeather(weatherComp, weather);
+ foreach (var (eProto, weather) in weatherComp.Weather)
+ {
+ // Reset cooldown if it's an existing one.
+ if (eProto == proto?.ID)
+ {
+ weather.EndTime = endTime;
+
+ if (weather.State == WeatherState.Ending)
+ weather.State = WeatherState.Running;
+
+ Dirty(weatherComp);
+ continue;
+ }
+
+ // Speedrun
+ var end = Timing.CurTime + WeatherComponent.ShutdownTime;
+
+ if (weather.EndTime == null || weather.EndTime > end)
+ {
+ weather.EndTime = end;
+ Dirty(weatherComp);
+ }
+ }
+
+ if (proto != null)
+ StartWeather(weatherComp, proto, endTime);
}
/// <summary>
/// Run every tick when the weather is running.
/// </summary>
- protected virtual void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime) {}
+ protected virtual void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) {}
- protected void StartWeather(WeatherComponent component, WeatherPrototype weather)
+ protected void StartWeather(WeatherComponent component, WeatherPrototype weather, TimeSpan? endTime)
{
- component.Weather = weather.ID;
- // TODO: ENGINE PR
- var duration = _random.NextDouble(weather.DurationMinimum.TotalSeconds, weather.DurationMaximum.TotalSeconds);
- component.EndTime = Timing.CurTime + TimeSpan.FromSeconds(duration);
- component.StartTime = Timing.CurTime;
- DebugTools.Assert(component.State == WeatherState.Invalid);
+ if (component.Weather.ContainsKey(weather.ID))
+ return;
+
+ var data = new WeatherData()
+ {
+ StartTime = Timing.CurTime,
+ EndTime = endTime,
+ };
+
+ component.Weather.Add(weather.ID, data);
Dirty(component);
}
- protected virtual void EndWeather(WeatherComponent component)
+ protected virtual void EndWeather(EntityUid uid, WeatherComponent component, string proto)
{
- component.Stream?.Stop();
- component.Stream = null;
- component.Weather = null;
- component.StartTime = TimeSpan.Zero;
- component.EndTime = TimeSpan.Zero;
- component.State = WeatherState.Invalid;
+ if (!component.Weather.TryGetValue(proto, out var data))
+ return;
+
+ data.Stream?.Stop();
+ data.Stream = null;
+ component.Weather.Remove(proto);
Dirty(component);
}
- protected virtual bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype)
+ protected virtual bool SetState(WeatherState state, WeatherComponent component, WeatherData weather, WeatherPrototype weatherProto)
{
- if (component.State.Equals(state))
+ if (weather.State.Equals(state))
return false;
- component.State = state;
+ weather.State = state;
+ Dirty(component);
return true;
}
[Serializable, NetSerializable]
protected sealed class WeatherComponentState : ComponentState
{
- public string? Weather;
- public TimeSpan StartTime;
- public TimeSpan EndTime;
+ public Dictionary<string, WeatherData> Weather;
+
+ public WeatherComponentState(Dictionary<string, WeatherData> weather)
+ {
+ Weather = weather;
+ }
}
}
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Weather;
public sealed class WeatherComponent : Component
{
/// <summary>
- /// Currently running weather.
+ /// Currently running weathers
/// </summary>
- [ViewVariables, DataField("weather")]
- public string? Weather;
+ [ViewVariables, DataField("weather", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<WeatherData, WeatherPrototype>))]
+ public Dictionary<string, WeatherData> Weather = new();
- // now
+ public static readonly TimeSpan StartupTime = TimeSpan.FromSeconds(15);
+ public static readonly TimeSpan ShutdownTime = TimeSpan.FromSeconds(15);
+}
+
+[DataDefinition, Serializable, NetSerializable]
+public sealed class WeatherData
+{
+ // Client audio stream.
+ [NonSerialized]
public IPlayingAudioStream? Stream;
/// <summary>
- /// When the weather started.
+ /// When the weather started if relevant.
/// </summary>
- [ViewVariables, DataField("startTime")]
+ [ViewVariables, DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan StartTime = TimeSpan.Zero;
/// <summary>
/// When the applied weather will end.
/// </summary>
- [ViewVariables(VVAccess.ReadWrite), DataField("endTime")]
- public TimeSpan EndTime = TimeSpan.Zero;
+ [ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan? EndTime;
[ViewVariables]
- public TimeSpan Duration => EndTime - StartTime;
+ public TimeSpan Duration => EndTime == null ? TimeSpan.MaxValue : EndTime.Value - StartTime;
- [ViewVariables]
+ [DataField("state")]
public WeatherState State = WeatherState.Invalid;
+
+ [ViewVariables, NonSerialized]
+ public float LastAlpha;
+
+ [ViewVariables, NonSerialized]
+ public float LastOcclusion;
}
public enum WeatherState : byte
-using Content.Shared.Maps;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Utility;
namespace Content.Shared.Weather;
{
[IdDataField] public string ID { get; } = default!;
- /// <summary>
- /// Minimum duration for the weather.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan DurationMinimum = TimeSpan.FromSeconds(120);
-
- /// <summary>
- /// Maximum duration for the weather.
- /// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan DurationMaximum = TimeSpan.FromSeconds(300);
-
- [ViewVariables(VVAccess.ReadWrite), DataField("startupTime")]
- public TimeSpan StartupTime = TimeSpan.FromSeconds(30);
-
- [ViewVariables(VVAccess.ReadWrite), DataField("endTime")]
- public TimeSpan ShutdownTime = TimeSpan.FromSeconds(30);
-
[ViewVariables(VVAccess.ReadWrite), DataField("sprite", required: true)]
public SpriteSpecifier Sprite = default!;
- Space
isSubfloor: true
canWirecutter: true
+ weather: true
footstepSounds:
collection: FootstepPlating
friction: 0.5