diff --git a/Content.Client/Weather/WeatherOverlay.cs b/Content.Client/Weather/WeatherOverlay.cs index c85fe822d2..cfa745bf1e 100644 --- a/Content.Client/Weather/WeatherOverlay.cs +++ b/Content.Client/Weather/WeatherOverlay.cs @@ -46,7 +46,7 @@ public sealed class WeatherOverlay : Overlay return false; if (!_entManager.TryGetComponent(_mapManager.GetMapEntityId(args.MapId), out var weather) || - weather.Weather == null) + weather.Weather.Count == 0) { return false; } @@ -58,15 +58,19 @@ public sealed class WeatherOverlay : Overlay { var mapUid = _mapManager.GetMapEntityId(args.MapId); - if (!_entManager.TryGetComponent(mapUid, out var weather) || - weather.Weather == null || - !_protoManager.TryIndex(weather.Weather, out var weatherProto)) + if (!_entManager.TryGetComponent(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(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) diff --git a/Content.Client/Weather/WeatherSystem.cs b/Content.Client/Weather/WeatherSystem.cs index ab8c8ea95b..b94cad6253 100644 --- a/Content.Client/Weather/WeatherSystem.cs +++ b/Content.Client/Weather/WeatherSystem.cs @@ -17,14 +17,9 @@ public sealed class WeatherSystem : SharedWeatherSystem [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; @@ -41,9 +36,9 @@ public sealed class WeatherSystem : SharedWeatherSystem _overlayManager.RemoveOverlay(); } - 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; @@ -56,21 +51,21 @@ public sealed class WeatherSystem : SharedWeatherSystem // 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; @@ -138,81 +133,75 @@ public sealed class WeatherSystem : SharedWeatherSystem } } - 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(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(proto), weather.EndTime); + weather.LastAlpha = 0f; + } } } diff --git a/Content.Server/Weather/WeatherSystem.cs b/Content.Server/Weather/WeatherSystem.cs index 8dd3146abf..305cd3b9e0 100644 --- a/Content.Server/Weather/WeatherSystem.cs +++ b/Content.Server/Weather/WeatherSystem.cs @@ -26,19 +26,15 @@ public sealed class WeatherSystem : SharedWeatherSystem 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; } @@ -54,13 +50,36 @@ public sealed class WeatherSystem : SharedWeatherSystem 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(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(args[1], out var weatherProto)) { - SetWeather(mapId, weatherProto); + SetWeather(mapId, weatherProto, endTime); } else { diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs index 165a5a0ffb..d0985961b5 100644 --- a/Content.Shared/Weather/SharedWeatherSystem.cs +++ b/Content.Shared/Weather/SharedWeatherSystem.cs @@ -3,10 +3,8 @@ using Robust.Shared.Map; 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; @@ -15,8 +13,8 @@ public abstract class SharedWeatherSystem : EntitySystem [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!; @@ -29,7 +27,13 @@ public abstract class SharedWeatherSystem : EntitySystem 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 bodyQuery) @@ -56,6 +60,31 @@ public abstract class SharedWeatherSystem : EntitySystem } + 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); @@ -65,103 +94,141 @@ public abstract class SharedWeatherSystem : EntitySystem var curTime = Timing.CurTime; - foreach (var (comp, metadata) in EntityQuery()) + foreach (var comp in EntityQuery()) { - 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(comp.Weather, out var weatherProto)) - { - Sawmill.Error($"Unable to find weather prototype for {comp.Weather}, ending!"); - EndWeather(comp); - continue; - } - - 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; - - if (elapsed < weatherProto.StartupTime) + // Ended + if (endTime != null && endTime < curTime) { - SetState(uid, comp, WeatherState.Starting, weatherProto); + EndWeather(uid, comp, proto); + continue; } - } - // Run whatever code we need. - Run(uid, comp, weatherProto, comp.State, frameTime); + var remainingTime = endTime - curTime; + + // Admin messed up or the likes. + if (!ProtoMan.TryIndex(proto, out var weatherProto)) + { + Sawmill.Error($"Unable to find weather prototype for {comp.Weather}, ending!"); + EndWeather(uid, comp, proto); + continue; + } + + // Shutting down + if (endTime != null && remainingTime < WeatherComponent.ShutdownTime) + { + 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, weather, weatherProto, frameTime); + } } } - public void SetWeather(MapId mapId, WeatherPrototype? weather) + /// + /// Shuts down all existing weather and starts the new one if applicable. + /// + public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime) { var weatherComp = EnsureComp(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); } /// /// Run every tick when the weather is running. /// - 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 Weather; + + public WeatherComponentState(Dictionary weather) + { + Weather = weather; + } } } diff --git a/Content.Shared/Weather/WeatherComponent.cs b/Content.Shared/Weather/WeatherComponent.cs index 6cbdd5bdfa..b5e420582e 100644 --- a/Content.Shared/Weather/WeatherComponent.cs +++ b/Content.Shared/Weather/WeatherComponent.cs @@ -1,5 +1,8 @@ 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; @@ -7,31 +10,45 @@ namespace Content.Shared.Weather; public sealed class WeatherComponent : Component { /// - /// Currently running weather. + /// Currently running weathers /// - [ViewVariables, DataField("weather")] - public string? Weather; + [ViewVariables, DataField("weather", customTypeSerializer:typeof(PrototypeIdDictionarySerializer))] + public Dictionary 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; /// - /// When the weather started. + /// When the weather started if relevant. /// - [ViewVariables, DataField("startTime")] + [ViewVariables, DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan StartTime = TimeSpan.Zero; /// /// When the applied weather will end. /// - [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 diff --git a/Content.Shared/Weather/WeatherPrototype.cs b/Content.Shared/Weather/WeatherPrototype.cs index ddf4b14dae..e782d97e15 100644 --- a/Content.Shared/Weather/WeatherPrototype.cs +++ b/Content.Shared/Weather/WeatherPrototype.cs @@ -1,7 +1,5 @@ -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; @@ -11,24 +9,6 @@ public sealed class WeatherPrototype : IPrototype { [IdDataField] public string ID { get; } = default!; - /// - /// Minimum duration for the weather. - /// - [ViewVariables(VVAccess.ReadWrite)] - public TimeSpan DurationMinimum = TimeSpan.FromSeconds(120); - - /// - /// Maximum duration for the weather. - /// - [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!; diff --git a/Resources/Prototypes/Tiles/plating.yml b/Resources/Prototypes/Tiles/plating.yml index 191069c194..3800af6487 100644 --- a/Resources/Prototypes/Tiles/plating.yml +++ b/Resources/Prototypes/Tiles/plating.yml @@ -18,6 +18,7 @@ - Space isSubfloor: true canWirecutter: true + weather: true footstepSounds: collection: FootstepPlating friction: 0.5