diff --git a/Content.Server/Atmos/Components/WeatherDeviceComponent.cs b/Content.Server/Atmos/Components/WeatherDeviceComponent.cs index c9b06096fae..837fe4f0b88 100644 --- a/Content.Server/Atmos/Components/WeatherDeviceComponent.cs +++ b/Content.Server/Atmos/Components/WeatherDeviceComponent.cs @@ -1,7 +1,56 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Weather; +using Robust.Shared.Prototypes; + namespace Content.Server.Atmos.Components; [RegisterComponent] public sealed partial class WeatherDeviceComponent : Component { + [DataField] + public bool Enabled; + + [DataField] + public Dictionary KeyFramesByCycleState = new(); + + [DataField] + public TimeSpan EnabledChangeTime; + + [DataField] + public string DefaultTickSpan; + + public TimeSpan DefaultTickSpanCasted { get; set; } + + [DataField] + public WeatherStateMachine? StateMachine; + + public TimeSpan LastChanged = TimeSpan.Zero; + + +} + +[Serializable, DataDefinition] +public partial record WeatherCycleState +{ + [DataField] + public ProtoId? SetWeatherTo = null; + + [DataField] + public bool ResetWeather = false; + + [DataField] + public float? TargetTemperature = null; + + [DataField] + public TimeSpan? WeatherOff; + + [DataField] + public string? TickSpan; + + public TimeSpan? TickSpanCasted; + /// + /// Calculatable + /// + public float TickRate { get; set; } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereWeatherDeviceSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereWeatherDeviceSystem.cs index 0c8b43faaf9..1f491a65a4d 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereWeatherDeviceSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereWeatherDeviceSystem.cs @@ -1,36 +1,292 @@ +using System.Linq; using Content.Server.Atmos.Components; +using Content.Shared.Atmos; +using Content.Shared.Maps; +using Content.Shared.Weather; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Server.Atmos.EntitySystems; public sealed partial class AtmosphereWeatherDeviceSystem : EntitySystem { - private TimeSpan _lastChange; - private readonly TimeSpan _maxWait = TimeSpan.FromSeconds(15); - + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly IGameTiming _time = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; + [Dependency] private readonly SharedWeatherSystem _weather = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + private readonly Dictionary> _cache = new(); + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + + base.Initialize(); + } + + private void OnStartup(Entity ent, ref ComponentStartup args) + { + ent.Comp.EnabledChangeTime = _time.CurTime; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + _cache.Remove(ent.Comp); + } /// public override void Update(float frameTime) { - var diff = _time.CurTime - _lastChange; - if (diff < _maxWait) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var device, out var transform)) { - return; + if (!device.Enabled) + { + continue; + } + + device.StateMachine ??= GetStateMachine(device); + + var diff = _time.CurTime - device.LastChanged; + if (diff < (device.StateMachine.Current.TickSpanCasted ?? device.DefaultTickSpanCasted)) + { + return; + } + + device.LastChanged = _time.CurTime; + + var map = transform.MapID; + + if (device.StateMachine.TrySwitchState(_time, _time.CurTime, device.EnabledChangeTime)) + { + var targetWeather = device.StateMachine.Current.SetWeatherTo; + if (targetWeather != null) + { + var endTime = _time.CurTime + device.StateMachine.Current.WeatherOff + TimeSpan.FromSeconds(5); + var weather = _prototypeManager.Index(targetWeather.Value); + + _weather.SetWeather(map, weather, endTime); + } + } + + if (!transform.GridUid.HasValue) + { + continue; + } + + var gridUid = transform.GridUid.Value; + + EnsureComp(transform.MapUid!.Value, out var mapAtmosphere); + + var temperatureChange = device.StateMachine.Current.TickRate; + var targetTemperature = device.StateMachine.NextTargetTemp ?? device.StateMachine.Current.TargetTemperature; + if (temperatureChange != 0) + { + if (!_cache.TryGetValue(device, out var list)) + { + list = new List(); + + EnsureComp(gridUid, out var grid); + EnsureComp(gridUid, out var gridAtmosphereComponent); + + foreach (var (_, tile) in gridAtmosphereComponent.Tiles) + { + var vector = tile.GridIndices; + var entityCoordinates = new EntityCoordinates(gridUid, vector); + var refTile = _map.GetTileRef((gridUid, grid), entityCoordinates); + var tileDef = (ContentTileDefinition)_tileDefinitionManager[refTile.Tile.TypeId]; + if (refTile.Tile.IsEmpty || !tileDef.CanCrowbar && tile.Air is { Immutable: false }) + { + list.Add(tile); + } + } + + _cache.Add(device, list); + } + + Log.Debug("Changing temp for {count} tiles! {change}",list.Count, temperatureChange.ToString("F1")); + bool isCooling = temperatureChange < 0; + foreach (var tileAtmosphere in list) + { + var mixture = tileAtmosphere.Air!; + if (tileAtmosphere.Air == null) + { + continue; + } + + if ( + targetTemperature != 0 + && ( + isCooling && mixture.Temperature >= targetTemperature + || !isCooling && mixture.Temperature <= targetTemperature + ) + ) + { + mixture.Temperature += temperatureChange; + } + } + + var currentGas = mapAtmosphere.Mixture; + + if ( + targetTemperature != 0 + && ( + isCooling && currentGas.Temperature >= targetTemperature + || !isCooling && currentGas.Temperature <= targetTemperature + ) + ) + { + if (currentGas.Immutable) + { + currentGas = new GasMixture(); + currentGas.CopyFrom(currentGas); + } + + currentGas.Temperature += temperatureChange; + _atmosphere.SetMapAtmosphere(transform.MapUid!.Value, false, currentGas); + } + + + } } + } - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var device, out var transform)) + private WeatherStateMachine GetStateMachine(WeatherDeviceComponent device) + { + var deviceDefaultTickSpan = TimeSpan.Parse(device.DefaultTickSpan); + device.DefaultTickSpanCasted = deviceDefaultTickSpan; + + var byKeyFrame = device.KeyFramesByCycleState.ToDictionary( + x => TimeSpan.Parse(x.Key), + x => x.Value + ); + + var byKeyFrameSequence = byKeyFrame.Select(x => (Span:x.Key,State: x.Value)).ToArray(); + + (TimeSpan Span, WeatherCycleState State) lastWeatherChange = default; + for (int i = 0; i < byKeyFrameSequence.Length; i++) { - var gridUid = transform.GridUid; - EnsureComp(gridUid!.Value, out var comp); - foreach (var tileAtmosphere in comp.MapTiles) + var (currentSpan, currentState)= byKeyFrameSequence[i]; + + if (i + 1 < byKeyFrameSequence.Length) + { + var (nextSpan, nextState) = byKeyFrameSequence[i + 1]; + if (currentState.TargetTemperature.HasValue && nextState.TargetTemperature.HasValue) + { + if (currentState.TickSpan != null) + { + currentState.TickSpanCasted = TimeSpan.Parse(currentState.TickSpan); + } + + var tickSpan = currentState.TickSpanCasted ?? deviceDefaultTickSpan; + var tickCount = (nextSpan - currentSpan) / tickSpan; + + currentState.TickRate = (float)((nextState.TargetTemperature.Value - currentState.TargetTemperature.Value) / tickCount); + } + } + + if (currentState.ResetWeather) + { + lastWeatherChange.State!.WeatherOff = currentSpan - lastWeatherChange.Span; + } + + if (!currentState.SetWeatherTo.HasValue) { - if (tileAtmosphere.Air != null) - tileAtmosphere.Air.Temperature += 5; + continue; } + + if (lastWeatherChange == default) + { + lastWeatherChange = (currentSpan, currentState); + continue; + } + + lastWeatherChange.State!.WeatherOff = currentSpan - lastWeatherChange.Span; + lastWeatherChange = (currentSpan, currentState); } - _lastChange = _time.CurTime; + return new WeatherStateMachine(byKeyFrame, Log); } } + +public class WeatherStateMachine +{ + private readonly LinkedList<(TimeSpan Span, WeatherCycleState State)> _nodes; + private readonly ISawmill _sawmill; + private LinkedListNode<(TimeSpan Span, WeatherCycleState State)> _current; + private readonly long _fullCycleTime; + private readonly LinkedListNode<(TimeSpan Span, WeatherCycleState State)> _last; + + public WeatherStateMachine(Dictionary nodes, ISawmill sawmill) + { + _nodes = new LinkedList<(TimeSpan Span, WeatherCycleState State)>(nodes.Select(x=>(x.Key,x.Value))); + _current = _nodes.First!; + _last = _nodes.Last!; + _sawmill = sawmill; + _fullCycleTime = (long)nodes.Keys.Max().TotalSeconds; + } + + public bool TrySwitchState( + IGameTiming timing, + TimeSpan currentTimeInCycle, + TimeSpan deviceEnableChangedTime + ) + { + var timeSinceEnabled = currentTimeInCycle - deviceEnableChangedTime; + var currentSpan = TimeSpan.FromSeconds((long)timeSinceEnabled.TotalSeconds % _fullCycleTime); + + if (currentSpan < _current.Value.Span) + { + _current = _nodes.First!; + _sawmill.Debug( + "[{time}] Changed state! {partOfCycle}, {tempDiff}, {targetTemp} {weather}, {off}", + timing.CurTime.ToString(@"hh\:mm\:ss"), + _current.Value.Span.ToString(@"hh\:mm\:ss"), + _current.Value.State.SetWeatherTo?.Id, + _current.Value.State.TickRate.ToString("F1"), + _current.Value.State.TargetTemperature?.ToString("F1"), + _current.Value.State.WeatherOff?.ToString(@"hh\:mm\:ss") + ); + return true; + } + + if (currentSpan > _current.Value.Span && currentSpan < _current.Next!.Value.Span) + { + return false; + } + + var next = _current.Next; + do + { + if (next != null && currentSpan >= next.Value.Span) + { + _current = next; + _sawmill.Debug( + "[{time}] Changed state! {partOfCycle}, {tempDiff}, {targetTemp} {weather}, {off}", + timing.CurTime.ToString(@"hh\:mm\:ss"), + _current.Value.Span.ToString(@"hh\:mm\:ss"), + _current.Value.State.SetWeatherTo?.Id, + _current.Value.State.TickRate.ToString("F1"), + _current.Value.State.TargetTemperature?.ToString("F1"), + _current.Value.State.WeatherOff?.ToString(@"hh\:mm\:ss") + ); + return true; + } + + next = next!.Next; + } while (_current.Next != null && currentSpan > _current.Next.Value.Span); + + return false; + } + + + + public WeatherCycleState Current { get => _current.Value.State; } + public float? NextTargetTemp => _current.Next?.Value.State.TargetTemperature; +} diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/portable.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/portable.yml index a3df9293a4c..d124c9f52d2 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/portable.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/portable.yml @@ -182,13 +182,47 @@ - type: entity - id: SpaceHeater2 + id: WeatherDevice1 parent: [BaseMachinePowered, ConstructibleMachine] - name: space heater 2 - description: A bluespace technology device that alters local temperature. Commonly referred to as a "Space Heater". - suffix: Unanchored + name: weather device components: - type: WeatherDevice + enabled: true + defaultTickSpan: "00:00:05" + keyFramesByCycleState: + "00:00:00": + targetTemperature: 293.16 + "00:00:15": + targetTemperature: 283.16 + "00:00:45": + targetTemperature: 273.16 + setWeatherTo: SnowfallLight + "00:01:00": + targetTemperature: 250 + "00:01:30": + targetTemperature: 240 + setWeatherTo: SnowfallMedium + "00:01:45": + targetTemperature: 230 + setWeatherTo: SnowfallHeavy + "00:02:00": + targetTemperature: 230 + "00:02:15": + targetTemperature: 240 + setWeatherTo: SnowfallMedium + "00:02:30": + targetTemperature: 250 + setWeatherTo: SnowfallLight + "00:03:00": + targetTemperature: 260 + resetWeather: true + "00:03:15": + targetTemperature: 273.16 + "00:03:45": + targetTemperature: 283.16 + "00:04:00": + targetTemperature: 293.16 + - type: StationAiWhitelist - type: Transform anchored: false @@ -241,31 +275,6 @@ - type: GasThermoMachine temperatureTolerance: 0.2 atmospheric: true - - type: Damageable - damageContainer: StructuralInorganic - damageModifierSet: Metallic - - type: Destructible - thresholds: - - trigger: - !type:DamageTrigger - damage: 600 - behaviors: - - !type:DoActsBehavior - acts: [ "Destruction" ] - - trigger: - !type:DamageTrigger - damage: 300 - behaviors: - - !type:PlaySoundBehavior - sound: - collection: MetalBreak - - !type:SpawnEntitiesBehavior - spawn: - SheetSteel1: - min: 1 - max: 3 - - !type:DoActsBehavior - acts: [ "Destruction" ] - type: entity parent: SpaceHeater