Atmos pipe rework (#3833)

* Initial

* Cleanup a bunch of things

* some changes dunno

* RequireAnchored

* a

* stuff

* more work

* Lots of progress

* delete pipe visualizer

* a

* b

* pipenet and pipenode cleanup

* Fixes

* Adds GasValve

* Adds GasMiner

* Fix stuff, maybe?

* More fixes

* Ignored components on the client

* Adds thermomachine behavior, change a bunch of stuff

* Remove Anchored

* some work, but it's shitcode

* significantly more ECS

* ECS AtmosDevices

* Cleanup

* fix appearance

* when the pipe direction is sus

* Gas tanks and canisters

* pipe anchoring and stuff

* coding is my passion

* Unsafe pipes take longer to unanchor

* turns out we're no longer using eris canisters

* Gas canister inserted tank appearance, improvements

* Work on a bunch of appearances

* Scrubber appearance

* Reorganize AtmosphereSystem.Piping into a bunch of different systems

* Appearance for vent/scrubber/pump turns off when leaving atmosphere

* ThermoMachine appearance

* Cleanup gas tanks

* Remove passive gate unused imports

* remove old canister UI functionality

* PipeNode environment air, make everything use AssumeAir instead of merging manually

* a

* Reorganize atmos to follow new structure

* ?????

* Canister UI, restructure client

* Restructure shared

* Fix build tho

* listen, at least the canister UI works entirely...

* fix build : )

* Atmos device prototypes have names and descriptions

* gas canister ui slider doesn't jitter

* trinary prototypes

* sprite for miners

* ignore components

* fix YAML

* Fix port system doing useless thing

* Fix build

* fix thinking moment

* fix build again because

* canister direction

* pipenode is a word

* GasTank Air will throw on invalid states

* fix build....

* Unhardcode volume pump thresholds

* Volume pump and filter take time into account

* Rename Join/Leave atmosphere events to AtmosDeviceEnabled/Disabled Event

* Gas tank node volume is set by initial mixtuer

* I love node container
This commit is contained in:
Vera Aguilera Puerto
2021-06-19 13:25:05 +02:00
committed by GitHub
parent cfc3f2e7fc
commit a2b737d945
250 changed files with 3964 additions and 3163 deletions

View File

@@ -0,0 +1,72 @@
using Content.Client.Items.Components;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Atmos.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Client.Atmos.Components
{
[RegisterComponent]
internal class GasAnalyzerComponent : SharedGasAnalyzerComponent, IItemStatus
{
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
[ViewVariables] private GasAnalyzerDanger Danger { get; set; }
Control IItemStatus.MakeControl()
{
return new StatusControl(this);
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState is not GasAnalyzerComponentState state)
return;
Danger = state.Danger;
_uiUpdateNeeded = true;
}
private sealed class StatusControl : Control
{
private readonly GasAnalyzerComponent _parent;
private readonly RichTextLabel _label;
public StatusControl(GasAnalyzerComponent parent)
{
_parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
parent._uiUpdateNeeded = true;
}
/// <inheritdoc />
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_parent._uiUpdateNeeded)
{
return;
}
_parent._uiUpdateNeeded = false;
var color = _parent.Danger switch
{
GasAnalyzerDanger.Warning => "orange",
GasAnalyzerDanger.Hazard => "red",
_ => "green",
};
_label.SetMarkup(Loc.GetString("itemstatus-pressure-warn", ("color", color), ("danger", _parent.Danger)));
}
}
}
}

View File

@@ -0,0 +1,106 @@
#nullable enable
using System.Collections.Generic;
using Content.Client.Atmos.Overlays;
using Content.Shared.Atmos;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Client.Atmos.EntitySystems
{
[UsedImplicitly]
internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem, IResettingEntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly Dictionary<GridId, AtmosDebugOverlayMessage> _tileData =
new();
// Configuration set by debug commands and used by AtmosDebugOverlay {
/// <summary>Value source for display</summary>
public AtmosDebugOverlayMode CfgMode;
/// <summary>This is subtracted from value (applied before CfgScale)</summary>
public float CfgBase = 0;
/// <summary>The value is divided by this (applied after CfgBase)</summary>
public float CfgScale = Atmospherics.MolesCellStandard * 2;
/// <summary>Gas ID used by GasMoles mode</summary>
public int CfgSpecificGas = 0;
/// <summary>Uses black-to-white interpolation (as opposed to red-green-blue) for colourblind users</summary>
public bool CfgCBM = false;
// }
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<AtmosDebugOverlayMessage>(HandleAtmosDebugOverlayMessage);
SubscribeNetworkEvent<AtmosDebugOverlayDisableMessage>(HandleAtmosDebugOverlayDisableMessage);
_mapManager.OnGridRemoved += OnGridRemoved;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if(!overlayManager.HasOverlay<AtmosDebugOverlay>())
overlayManager.AddOverlay(new AtmosDebugOverlay());
}
private void HandleAtmosDebugOverlayMessage(AtmosDebugOverlayMessage message)
{
_tileData[message.GridId] = message;
}
private void HandleAtmosDebugOverlayDisableMessage(AtmosDebugOverlayDisableMessage ev)
{
_tileData.Clear();
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.OnGridRemoved -= OnGridRemoved;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if(!overlayManager.HasOverlay<GasTileOverlay>())
overlayManager.RemoveOverlay<GasTileOverlay>();
}
public void Reset()
{
_tileData.Clear();
}
private void OnGridRemoved(MapId mapId, GridId gridId)
{
if (_tileData.ContainsKey(gridId))
{
_tileData.Remove(gridId);
}
}
public bool HasData(GridId gridId)
{
return _tileData.ContainsKey(gridId);
}
public AtmosDebugOverlayData? GetData(GridId gridIndex, Vector2i indices)
{
if (!_tileData.TryGetValue(gridIndex, out var srcMsg))
return null;
var relative = indices - srcMsg.BaseIdx;
if (relative.X < 0 || relative.Y < 0 || relative.X >= LocalViewRange || relative.Y >= LocalViewRange)
return null;
return srcMsg.OverlayData[relative.X + (relative.Y * LocalViewRange)];
}
}
internal enum AtmosDebugOverlayMode : byte
{
TotalMoles,
GasMoles,
Temperature
}
}

View File

@@ -0,0 +1,10 @@
using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
namespace Content.Client.Atmos.EntitySystems
{
[UsedImplicitly]
public class AtmosphereSystem : SharedAtmosphereSystem
{
}
}

View File

@@ -0,0 +1,206 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Client.Atmos.Overlays;
using Content.Shared.Atmos;
using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Client.Atmos.EntitySystems
{
[UsedImplicitly]
internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
// Gas overlays
private readonly float[] _timer = new float[Atmospherics.TotalNumberOfGases];
private readonly float[][] _frameDelays = new float[Atmospherics.TotalNumberOfGases][];
private readonly int[] _frameCounter = new int[Atmospherics.TotalNumberOfGases];
private readonly Texture[][] _frames = new Texture[Atmospherics.TotalNumberOfGases][];
// Fire overlays
private const int FireStates = 3;
private const string FireRsiPath = "/Textures/Effects/fire.rsi";
private readonly float[] _fireTimer = new float[FireStates];
private readonly float[][] _fireFrameDelays = new float[FireStates][];
private readonly int[] _fireFrameCounter = new int[FireStates];
private readonly Texture[][] _fireFrames = new Texture[FireStates][];
private readonly Dictionary<GridId, Dictionary<Vector2i, GasOverlayChunk>> _tileData =
new();
private AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<GasOverlayMessage>(HandleGasOverlayMessage);
_mapManager.OnGridRemoved += OnGridRemoved;
_atmosphereSystem = Get<AtmosphereSystem>();
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var overlay = _atmosphereSystem.GetOverlay(i);
switch (overlay)
{
case SpriteSpecifier.Rsi animated:
var rsi = _resourceCache.GetResource<RSIResource>(animated.RsiPath).RSI;
var stateId = animated.RsiState;
if(!rsi.TryGetState(stateId, out var state)) continue;
_frames[i] = state.GetFrames(RSI.State.Direction.South);
_frameDelays[i] = state.GetDelays();
_frameCounter[i] = 0;
break;
case SpriteSpecifier.Texture texture:
_frames[i] = new[] {texture.Frame0()};
_frameDelays[i] = Array.Empty<float>();
break;
case null:
_frames[i] = Array.Empty<Texture>();
_frameDelays[i] = Array.Empty<float>();
break;
}
}
var fire = _resourceCache.GetResource<RSIResource>(FireRsiPath).RSI;
for (var i = 0; i < FireStates; i++)
{
if (!fire.TryGetState((i+1).ToString(), out var state))
throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!");
_fireFrames[i] = state.GetFrames(RSI.State.Direction.South);
_fireFrameDelays[i] = state.GetDelays();
_fireFrameCounter[i] = 0;
}
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if(!overlayManager.HasOverlay<GasTileOverlay>())
overlayManager.AddOverlay(new GasTileOverlay());
}
private void HandleGasOverlayMessage(GasOverlayMessage message)
{
foreach (var (indices, data) in message.OverlayData)
{
var chunk = GetOrCreateChunk(message.GridId, indices);
chunk.Update(data, indices);
}
}
// Slightly different to the server-side system version
private GasOverlayChunk GetOrCreateChunk(GridId gridId, Vector2i indices)
{
if (!_tileData.TryGetValue(gridId, out var chunks))
{
chunks = new Dictionary<Vector2i, GasOverlayChunk>();
_tileData[gridId] = chunks;
}
var chunkIndices = GetGasChunkIndices(indices);
if (!chunks.TryGetValue(chunkIndices, out var chunk))
{
chunk = new GasOverlayChunk(gridId, chunkIndices);
chunks[chunkIndices] = chunk;
}
return chunk;
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.OnGridRemoved -= OnGridRemoved;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if(!overlayManager.HasOverlay<GasTileOverlay>())
overlayManager.RemoveOverlay<GasTileOverlay>();
}
private void OnGridRemoved(MapId mapId, GridId gridId)
{
if (_tileData.ContainsKey(gridId))
{
_tileData.Remove(gridId);
}
}
public bool HasData(GridId gridId)
{
return _tileData.ContainsKey(gridId);
}
public IEnumerable<(Texture, Color)> GetOverlays(GridId gridIndex, Vector2i indices)
{
if (!_tileData.TryGetValue(gridIndex, out var chunks))
yield break;
var chunkIndex = GetGasChunkIndices(indices);
if (!chunks.TryGetValue(chunkIndex, out var chunk))
yield break;
var overlays = chunk.GetData(indices);
if (overlays.Gas == null)
yield break;
var fire = overlays.FireState != 0;
foreach (var gasData in overlays.Gas)
{
var frames = _frames[gasData.Index];
yield return (frames[_frameCounter[gasData.Index]], Color.White.WithAlpha(gasData.Opacity));
}
if (fire)
{
var state = overlays.FireState - 1;
var frames = _fireFrames[state];
// TODO ATMOS Set color depending on temperature
yield return (frames[_fireFrameCounter[state]], Color.White);
}
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var delays = _frameDelays[i];
if (delays.Length == 0) continue;
var frameCount = _frameCounter[i];
_timer[i] += frameTime;
if (!(_timer[i] >= delays[frameCount])) continue;
_timer[i] = 0f;
_frameCounter[i] = (frameCount + 1) % _frames[i].Length;
}
for (var i = 0; i < FireStates; i++)
{
var delays = _fireFrameDelays[i];
if (delays.Length == 0) continue;
var frameCount = _fireFrameCounter[i];
_fireTimer[i] += frameTime;
if (!(_fireTimer[i] >= delays[frameCount])) continue;
_fireTimer[i] = 0f;
_fireFrameCounter[i] = (frameCount + 1) % _fireFrames[i].Length;
}
}
}
}

View File

@@ -1,15 +1,14 @@
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.Atmos;
using Content.Client.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.EntitySystems;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Enums;
using System;
namespace Content.Client.Atmos
namespace Content.Client.Atmos.Overlays
{
public class AtmosDebugOverlay : Overlay
{

View File

@@ -1,12 +1,12 @@
using Content.Client.GameObjects.EntitySystems;
using Robust.Shared.Enums;
using Content.Client.Atmos.EntitySystems;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Client.Atmos
namespace Content.Client.Atmos.Overlays
{
public class GasTileOverlay : Overlay
{

View File

@@ -0,0 +1,39 @@
using System;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public abstract class EnabledAtmosDeviceVisualizer : AppearanceVisualizer
{
[DataField("enabledState")]
private string _enabledState = string.Empty;
protected abstract object LayerMap { get; }
protected abstract Enum DataKey { get; }
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
if (!entity.TryGetComponent(out ISpriteComponent? sprite))
return;
sprite.LayerMapSet(LayerMap, sprite.AddLayerState(_enabledState));
sprite.LayerSetVisible(LayerMap, false);
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite))
return;
if(component.TryGetData(DataKey, out bool enabled))
sprite.LayerSetVisible(LayerMap, enabled);
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public class OutletInjectorVisualizer : EnabledAtmosDeviceVisualizer
{
protected override object LayerMap => Layers.Enabled;
protected override Enum DataKey => OutletInjectorVisuals.Enabled;
enum Layers
{
Enabled,
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public class PassiveVentVisualizer : EnabledAtmosDeviceVisualizer
{
protected override object LayerMap => Layers.Enabled;
protected override Enum DataKey => PassiveVentVisuals.Enabled;
enum Layers
{
Enabled,
}
}
}

View File

@@ -0,0 +1,86 @@
#nullable enable
using System;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public class PipeConnectorVisualizer : AppearanceVisualizer, ISerializationHooks
{
[DataField("rsi")]
private string _rsi = "Constructible/Atmos/pipe.rsi";
[DataField("baseState")]
private string _baseState = "pipeConnector";
private RSI? _connectorRsi;
void ISerializationHooks.AfterDeserialization()
{
var rsiString = SharedSpriteComponent.TextureRoot / _rsi;
var resourceCache = IoCManager.Resolve<IResourceCache>();
if (resourceCache.TryGetResource(rsiString, out RSIResource? rsi))
_connectorRsi = rsi.RSI;
else
Logger.Error($"{nameof(PipeConnectorVisualizer)} could not load to load RSI {rsiString}.");
}
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
if (!entity.TryGetComponent<ISpriteComponent>(out var sprite))
return;
if (_connectorRsi == null)
return;
foreach (Layer layerKey in Enum.GetValues(typeof(Layer)))
{
sprite.LayerMapReserveBlank(layerKey);
var layer = sprite.LayerMapGet(layerKey);
sprite.LayerSetRSI(layer, _connectorRsi);
var layerState = _baseState + ((PipeDirection) layerKey).ToString();
sprite.LayerSetState(layer, layerState);
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent<ISpriteComponent>(out var sprite))
return;
if (!component.TryGetData(PipeVisuals.VisualState, out PipeVisualState state))
return;
foreach (Layer layerKey in Enum.GetValues(typeof(Layer)))
{
var dir = (PipeDirection) layerKey;
var layerVisible = state.ConnectedDirections.HasDirection(dir);
var layer = sprite.LayerMapGet(layerKey);
sprite.LayerSetVisible(layer, layerVisible);
}
}
private enum Layer : byte
{
NorthConnection = PipeDirection.North,
SouthConnection = PipeDirection.South,
EastConnection = PipeDirection.East,
WestConnection = PipeDirection.West,
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public class PressurePumpVisualizer : EnabledAtmosDeviceVisualizer
{
protected override object LayerMap => Layers.Enabled;
protected override Enum DataKey => PressurePumpVisuals.Enabled;
enum Layers
{
Enabled,
}
}
}

View File

@@ -0,0 +1,52 @@
using Content.Shared.Atmos.Piping.Unary.Visuals;
using Content.Shared.Atmos.Visuals;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public class ScrubberVisualizer : AppearanceVisualizer
{
private string _offState = "scrub_off";
private string _scrubState = "scrub_on";
private string _siphonState = "scrub_purge";
private string _weldedState = "scrub_welded";
private string _wideState = "scrub_wide";
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite))
return;
if (!component.TryGetData(ScrubberVisuals.State, out ScrubberState state))
return;
switch (state)
{
case ScrubberState.Off:
sprite.LayerSetState(ScrubberVisualLayers.Scrubber, _offState);
break;
case ScrubberState.Scrub:
sprite.LayerSetState(ScrubberVisualLayers.Scrubber, _scrubState);
break;
case ScrubberState.Siphon:
sprite.LayerSetState(ScrubberVisualLayers.Scrubber, _siphonState);
break;
case ScrubberState.Welded:
sprite.LayerSetState(ScrubberVisualLayers.Scrubber, _weldedState);
break;
case ScrubberState.WideScrub:
sprite.LayerSetState(ScrubberVisualLayers.Scrubber, _wideState);
break;
}
}
}
public enum ScrubberVisualLayers
{
Scrubber,
}
}

View File

@@ -0,0 +1,18 @@
using System;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public class ThermoMachineVisualizer : EnabledAtmosDeviceVisualizer
{
protected override object LayerMap => Layers.Enabled;
protected override Enum DataKey => ThermoMachineVisuals.Enabled;
enum Layers
{
Enabled,
}
}
}

View File

@@ -0,0 +1,47 @@
using Content.Shared.Atmos.Visuals;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Atmos.Piping
{
[UsedImplicitly]
public class VentPumpVisualizer : AppearanceVisualizer
{
private string _offState = "vent_off";
private string _inState = "vent_in";
private string _outState = "vent_out";
private string _weldedState = "vent_welded";
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite))
return;
if (!component.TryGetData(VentPumpVisuals.State, out VentPumpState state))
return;
switch (state)
{
case VentPumpState.Off:
sprite.LayerSetState(VentVisualLayers.Vent, _offState);
break;
case VentPumpState.In:
sprite.LayerSetState(VentVisualLayers.Vent, _inState);
break;
case VentPumpState.Out:
sprite.LayerSetState(VentVisualLayers.Vent, _outState);
break;
case VentPumpState.Welded:
sprite.LayerSetState(VentVisualLayers.Vent, _weldedState);
break;
}
}
}
public enum VentVisualLayers
{
Vent,
}
}

View File

@@ -0,0 +1,43 @@
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using static Content.Shared.Atmos.Components.SharedGasAnalyzerComponent;
namespace Content.Client.Atmos.UI
{
public class GasAnalyzerBoundUserInterface : BoundUserInterface
{
public GasAnalyzerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
private GasAnalyzerWindow? _menu;
protected override void Open()
{
base.Open();
_menu = new GasAnalyzerWindow(this);
_menu.OnClose += Close;
_menu.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
_menu?.Populate((GasAnalyzerBoundUserInterfaceState) state);
}
public void Refresh()
{
SendMessage(new GasAnalyzerRefreshMessage());
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing) _menu?.Dispose();
}
}
}

View File

@@ -0,0 +1,264 @@
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Temperature;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Content.Shared.Atmos.Components.SharedGasAnalyzerComponent;
namespace Content.Client.Atmos.UI
{
public class GasAnalyzerWindow : BaseWindow
{
public GasAnalyzerBoundUserInterface Owner { get; }
private readonly Control _topContainer;
private readonly Control _statusContainer;
private readonly Label _nameLabel;
public TextureButton CloseButton { get; set; }
public GasAnalyzerWindow(GasAnalyzerBoundUserInterface owner)
{
var resourceCache = IoCManager.Resolve<IResourceCache>();
Owner = owner;
var rootContainer = new LayoutContainer {Name = "WireRoot"};
AddChild(rootContainer);
MouseFilter = MouseFilterMode.Stop;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
{
Texture = panelTex,
Modulate = Color.FromHex("#25252A"),
};
back.SetPatchMargin(StyleBox.Margin.All, 10);
var topPanel = new PanelContainer
{
PanelOverride = back,
MouseFilter = MouseFilterMode.Pass
};
var bottomWrap = new LayoutContainer
{
Name = "BottomWrap"
};
rootContainer.AddChild(topPanel);
rootContainer.AddChild(bottomWrap);
LayoutContainer.SetAnchorPreset(topPanel, LayoutContainer.LayoutPreset.Wide);
LayoutContainer.SetMarginBottom(topPanel, -80);
LayoutContainer.SetAnchorPreset(bottomWrap, LayoutContainer.LayoutPreset.VerticalCenterWide);
LayoutContainer.SetGrowHorizontal(bottomWrap, LayoutContainer.GrowDirection.Both);
var topContainerWrap = new VBoxContainer
{
Children =
{
(_topContainer = new VBoxContainer()),
new Control {MinSize = (0, 110)}
}
};
rootContainer.AddChild(topContainerWrap);
LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide);
var font = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
var fontSmall = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 10);
Button refreshButton;
var topRow = new HBoxContainer
{
Margin = new Thickness(4, 4, 12, 2),
Children =
{
(_nameLabel = new Label
{
Text = Loc.GetString("Gas Analyzer"),
FontOverride = font,
FontColorOverride = StyleNano.NanoGold,
VerticalAlignment = VAlignment.Center
}),
new Control
{
MinSize = (20, 0),
HorizontalExpand = true,
},
(refreshButton = new Button {Text = "Refresh"}), //TODO: refresh icon?
new Control
{
MinSize = (2, 0),
},
(CloseButton = new TextureButton
{
StyleClasses = {SS14Window.StyleClassWindowCloseButton},
VerticalAlignment = VAlignment.Center
})
}
};
refreshButton.OnPressed += a =>
{
Owner.Refresh();
};
var middle = new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#202025")},
Children =
{
(_statusContainer = new VBoxContainer
{
Margin = new Thickness(8, 8, 4, 4)
})
}
};
_topContainer.AddChild(topRow);
_topContainer.AddChild(new PanelContainer
{
MinSize = (0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")}
});
_topContainer.AddChild(middle);
_topContainer.AddChild(new PanelContainer
{
MinSize = (0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")}
});
CloseButton.OnPressed += _ => Close();
SetSize = (300, 200);
}
public void Populate(GasAnalyzerBoundUserInterfaceState state)
{
_statusContainer.RemoveAllChildren();
if (state.Error != null)
{
_statusContainer.AddChild(new Label
{
Text = Loc.GetString("Error: {0}", state.Error),
FontColorOverride = Color.Red
});
return;
}
_statusContainer.AddChild(new Label
{
Text = Loc.GetString("Pressure: {0:0.##} kPa", state.Pressure)
});
_statusContainer.AddChild(new Label
{
Text = Loc.GetString("Temperature: {0:0.#}K ({1:0.#}°C)", state.Temperature,
TemperatureHelpers.KelvinToCelsius(state.Temperature))
});
// Return here cause all that stuff down there is gas stuff (so we don't get the seperators)
if (state.Gases == null || state.Gases.Length == 0)
{
return;
}
// Seperator
_statusContainer.AddChild(new Control
{
MinSize = new Vector2(0, 10)
});
// Add a table with all the gases
var tableKey = new VBoxContainer();
var tableVal = new VBoxContainer();
_statusContainer.AddChild(new HBoxContainer
{
Children =
{
tableKey,
new Control
{
MinSize = new Vector2(20, 0)
},
tableVal
}
});
// This is the gas bar thingy
var height = 30;
var minSize = 24; // This basically allows gases which are too small, to be shown properly
var gasBar = new HBoxContainer
{
HorizontalExpand = true,
MinSize = new Vector2(0, height)
};
// Seperator
_statusContainer.AddChild(new Control
{
MinSize = new Vector2(0, 10)
});
var totalGasAmount = 0f;
foreach (var gas in state.Gases)
{
totalGasAmount += gas.Amount;
}
for (int i = 0; i < state.Gases.Length; i++)
{
var gas = state.Gases[i];
var color = Color.FromHex($"#{gas.Color}", Color.White);
// Add to the table
tableKey.AddChild(new Label
{
Text = Loc.GetString(gas.Name)
});
tableVal.AddChild(new Label
{
Text = Loc.GetString("{0:0.##} mol", gas.Amount)
});
// Add to the gas bar //TODO: highlight the currently hover one
var left = (i == 0) ? 0f : 2f;
var right = (i == state.Gases.Length - 1) ? 0f : 2f;
gasBar.AddChild(new PanelContainer
{
ToolTip = Loc.GetString("{0}: {1:0.##} mol ({2:0.#}%)", gas.Name, gas.Amount,
(gas.Amount / totalGasAmount) * 100),
HorizontalExpand = true,
SizeFlagsStretchRatio = gas.Amount,
MouseFilter = MouseFilterMode.Pass,
PanelOverride = new StyleBoxFlat
{
BackgroundColor = color,
PaddingLeft = left,
PaddingRight = right
},
MinSize = new Vector2(minSize, 0)
});
}
_statusContainer.AddChild(gasBar);
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
return DragMode.Move;
}
protected override bool HasPoint(Vector2 point)
{
// This makes it so our base window won't count for hit tests,
// but we will still receive mouse events coming in from Pass mouse filter mode.
// So basically, it perfectly shells out the hit tests to the panels we have!
return false;
}
}
}

View File

@@ -0,0 +1,86 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Client.Atmos.UI
{
/// <summary>
/// Initializes a <see cref="GasCanisterWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public class GasCanisterBoundUserInterface : BoundUserInterface
{
private GasCanisterWindow? _window;
public GasCanisterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new GasCanisterWindow();
if(State != null)
UpdateState(State);
_window.OpenCentered();
_window.OnClose += Close;
_window.ReleaseValveCloseButtonPressed += OnReleaseValveClosePressed;
_window.ReleaseValveOpenButtonPressed += OnReleaseValveOpenPressed;
_window.ReleasePressureSliderChanged += OnReleasePressurePressed;
_window.TankEjectButtonPressed += OnTankEjectPressed;
}
private void OnTankEjectPressed()
{
SendMessage(new GasCanisterHoldingTankEjectMessage());
}
private void OnReleasePressurePressed(float value)
{
SendMessage(new GasCanisterChangeReleasePressureMessage(value));
}
private void OnReleaseValveOpenPressed()
{
SendMessage(new GasCanisterChangeReleaseValveMessage(true));
}
private void OnReleaseValveClosePressed()
{
SendMessage(new GasCanisterChangeReleaseValveMessage(false));
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast)
return;
_window.SetCanisterLabel(cast.CanisterLabel);
_window.SetCanisterPressure(cast.CanisterPressure);
_window.SetPortStatus(cast.PortStatus);
_window.SetTankLabel(cast.TankLabel);
_window.SetTankPressure(cast.TankPressure);
_window.SetReleasePressureRange(cast.ReleasePressureMin, cast.ReleasePressureMax);
_window.SetReleasePressure(cast.ReleasePressure);
_window.SetReleaseValve(cast.ReleaseValve);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
_window?.Dispose();
}
}
}

View File

@@ -0,0 +1,52 @@
<SS14Window xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:Content.Client.Stylesheets"
MinSize="480 400" Title="Canister">
<VBoxContainer Margin="5 5 5 5" SeparationOverride="10">
<VBoxContainer VerticalExpand="True">
<Label Text="{Loc comp-gas-canister-ui-canister-status}" FontColorOverride="{x:Static s:StyleNano.NanoGold}" StyleClasses="LabelBig"/>
<HBoxContainer>
<Label Text="{Loc comp-gas-canister-ui-canister-pressure}"/>
<Label Name="CanisterPressureLabel" Align="Center" HorizontalExpand="True"/>
</HBoxContainer>
<HBoxContainer>
<Label Text="{Loc comp-gas-canister-ui-port-status}"/>
<Label Name="PortStatusLabel" Align="Center" HorizontalExpand="True"/>
</HBoxContainer>
</VBoxContainer>
<VBoxContainer VerticalExpand="True">
<Label Text="{Loc comp-gas-canister-ui-holding-tank-status}" FontColorOverride="{x:Static s:StyleNano.NanoGold}" StyleClasses="LabelBig"/>
<HBoxContainer>
<Label Text="{Loc comp-gas-canister-ui-holding-tank-label}"/>
<Label Name="TankLabelLabel" Text="{Loc comp-gas-canister-ui-holding-tank-label-empty}" Align="Center" HorizontalExpand="True"/>
<Button Name="TankEjectButton" Text="{Loc comp-gas-canister-ui-holding-tank-eject}"/>
</HBoxContainer>
<HBoxContainer>
<Label Text="{Loc comp-gas-canister-ui-holding-tank-pressure}"/>
<Label Name="TankPressureLabel" Align="Center" HorizontalExpand="True"/>
</HBoxContainer>
</VBoxContainer>
<VBoxContainer VerticalExpand="True">
<Label Text="{Loc comp-gas-canister-ui-release-valve-status}" FontColorOverride="{x:Static s:StyleNano.NanoGold}" StyleClasses="LabelBig"/>
<HBoxContainer>
<VBoxContainer>
<Label Text="{Loc comp-gas-canister-ui-release-pressure}"/>
<Control VerticalExpand="True"/>
</VBoxContainer>
<VBoxContainer HorizontalExpand="True" Margin="15 0 0 15" SeparationOverride="5">
<Slider Name="ReleasePressureSlider" HorizontalExpand="True"/>
<Label Name="ReleasePressureLabel" Align="Center" HorizontalExpand="True"/>
</VBoxContainer>
</HBoxContainer>
<HBoxContainer>
<Label Text="{Loc comp-gas-canister-ui-release-valve}"/>
<Control HorizontalExpand="True"/>
<Button Name="ReleaseValveOpenButton" Text="{Loc comp-gas-canister-ui-release-valve-open}" ToggleMode="True"/>
<Button Name="ReleaseValveCloseButton" Text="{Loc comp-gas-canister-ui-release-valve-close}" ToggleMode="True"/>
<Control HorizontalExpand="True"/>
</HBoxContainer>
</VBoxContainer>
</VBoxContainer>
</SS14Window>

View File

@@ -0,0 +1,98 @@
using System;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Atmos.UI
{
/// <summary>
/// Client-side UI used to control a canister.
/// </summary>
[GenerateTypedNameReferences]
public partial class GasCanisterWindow : SS14Window
{
private readonly ButtonGroup _buttonGroup = new();
public event Action? TankEjectButtonPressed;
public event Action<float>? ReleasePressureSliderChanged;
public event Action? ReleaseValveCloseButtonPressed;
public event Action? ReleaseValveOpenButtonPressed;
public GasCanisterWindow()
{
RobustXamlLoader.Load(this);
ReleaseValveCloseButton.Group = _buttonGroup;
ReleaseValveOpenButton.Group = _buttonGroup;
ReleaseValveCloseButton.OnPressed += _ => ReleaseValveCloseButtonPressed?.Invoke();
ReleaseValveOpenButton.OnPressed += _ => ReleaseValveOpenButtonPressed?.Invoke();
TankEjectButton.OnPressed += _ => TankEjectButtonPressed?.Invoke();
ReleasePressureSlider.OnValueChanged += r => ReleasePressureSliderChanged?.Invoke(r.Value);
}
public void SetCanisterLabel(string label)
{
Title = label;
}
public void SetCanisterPressure(float pressure)
{
CanisterPressureLabel.Text = Loc.GetString("comp-gas-canister-ui-pressure", ("pressure", Math.Round(pressure)));
}
public void SetPortStatus(bool status)
{
if (status)
{
PortStatusLabel.Text = Loc.GetString("comp-gas-canister-ui-port-connected");
}
else
{
PortStatusLabel.Text = Loc.GetString("comp-gas-canister-ui-port-disconnected");
}
}
public void SetTankLabel(string? label)
{
if (label == null)
{
TankEjectButton.Disabled = true;
TankLabelLabel.Text = Loc.GetString("comp-gas-canister-ui-holding-tank-label-empty");
return;
}
TankEjectButton.Disabled = false;
TankLabelLabel.Text = label;
}
public void SetTankPressure(float pressure)
{
TankPressureLabel.Text = Loc.GetString("comp-gas-canister-ui-pressure", ("pressure", Math.Round(pressure)));
}
public void SetReleasePressureRange(float min, float max)
{
ReleasePressureSlider.MinValue = min;
ReleasePressureSlider.MaxValue = max;
}
public void SetReleasePressure(float pressure)
{
if(!ReleasePressureSlider.Grabbed)
ReleasePressureSlider.SetValueWithoutEvent(pressure);
ReleasePressureLabel.Text = Loc.GetString("comp-gas-canister-ui-pressure", ("pressure", Math.Round(pressure)));
}
public void SetReleaseValve(bool valve)
{
if (valve)
ReleaseValveOpenButton.Pressed = true;
else
ReleaseValveCloseButton.Pressed = true;
}
}
}

View File

@@ -0,0 +1,37 @@
using Content.Shared.Atmos.Visuals;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Visualizers
{
[UsedImplicitly]
public class AtmosPlaqueVisualizer : AppearanceVisualizer
{
[DataField("layer")]
private int Layer { get; }
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
entity.GetComponentOrNull<SpriteComponent>()?.LayerMapReserveBlank(Layer);
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out SpriteComponent? sprite))
{
return;
}
if (!component.TryGetData(AtmosPlaqueVisuals.State, out string state))
{
sprite.LayerSetState(Layer, state);
}
}
}
}

View File

@@ -0,0 +1,71 @@
using Content.Shared.Atmos.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Visualizers
{
[UsedImplicitly]
public class FireVisualizer : AppearanceVisualizer
{
[DataField("fireStackAlternateState")]
private int _fireStackAlternateState = 3;
[DataField("normalState")]
private string? _normalState;
[DataField("alternateState")]
private string? _alternateState;
[DataField("sprite")]
private string? _sprite;
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
var sprite = entity.GetComponent<ISpriteComponent>();
sprite.LayerMapReserveBlank(FireVisualLayers.Fire);
sprite.LayerSetVisible(FireVisualLayers.Fire, false);
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.TryGetData(FireVisuals.OnFire, out bool onFire))
{
var fireStacks = 0f;
if (component.TryGetData(FireVisuals.FireStacks, out float stacks))
fireStacks = stacks;
SetOnFire(component, onFire, fireStacks);
}
}
private void SetOnFire(AppearanceComponent component, bool onFire, float fireStacks)
{
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if (_sprite != null)
{
sprite.LayerSetRSI(FireVisualLayers.Fire, _sprite);
}
sprite.LayerSetVisible(FireVisualLayers.Fire, onFire);
if(fireStacks > _fireStackAlternateState && !string.IsNullOrEmpty(_alternateState))
sprite.LayerSetState(FireVisualLayers.Fire, _alternateState);
else
sprite.LayerSetState(FireVisualLayers.Fire, _normalState);
}
}
public enum FireVisualLayers : byte
{
Fire
}
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Atmos.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Visualizers
{
[UsedImplicitly]
public class GasAnalyzerVisualizer : AppearanceVisualizer
{
[DataField("state_off")]
private string? _stateOff;
[DataField("state_working")]
private string? _stateWorking;
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.Deleted)
{
return;
}
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite))
{
return;
}
if (component.TryGetData(GasAnalyzerVisuals.VisualState, out GasAnalyzerVisualState visualState))
{
switch (visualState)
{
case GasAnalyzerVisualState.Off:
sprite.LayerSetState(0, _stateOff);
break;
case GasAnalyzerVisualState.Working:
sprite.LayerSetState(0, _stateWorking);
break;
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Visualizers
{
[UsedImplicitly]
public class GasCanisterVisualizer : AppearanceVisualizer
{
[DataField("pressureStates")]
private readonly string[] _statePressure = {"", "", "", ""};
[DataField("insertedTankState")]
private readonly string _insertedTankState = string.Empty;
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
var sprite = entity.GetComponent<ISpriteComponent>();
sprite.LayerMapSet(Layers.PressureLight, sprite.AddLayerState(_statePressure[0]));
sprite.LayerSetShader(Layers.PressureLight, "unshaded");
sprite.LayerMapSet(Layers.TankInserted, sprite.AddLayerState(_insertedTankState));
sprite.LayerSetVisible(Layers.TankInserted, false);
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite))
{
return;
}
// Update the canister lights
if (component.TryGetData(GasCanisterVisuals.PressureState, out int pressureState))
if ((pressureState >= 0) && (pressureState < _statePressure.Length))
sprite.LayerSetState(Layers.PressureLight, _statePressure[pressureState]);
if(component.TryGetData(GasCanisterVisuals.TankInserted, out bool inserted))
sprite.LayerSetVisible(Layers.TankInserted, inserted);
}
private enum Layers
{
PressureLight,
TankInserted,
}
}
}

View File

@@ -0,0 +1,54 @@
using Content.Shared.Atmos.Piping.Unary.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Visualizers
{
[UsedImplicitly]
public class GasPortableVisualizer : AppearanceVisualizer
{
[DataField("stateConnected")]
private string? _stateConnected;
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
var sprite = entity.GetComponent<ISpriteComponent>();
if (_stateConnected != null)
{
sprite.LayerMapSet(Layers.ConnectedToPort, sprite.AddLayerState(_stateConnected));
sprite.LayerSetVisible(Layers.ConnectedToPort, false);
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.Deleted)
{
return;
}
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite))
{
return;
}
// Update the visuals : Is the canister connected to a port or not
if (component.TryGetData(GasPortableVisuals.ConnectedState, out bool isConnected))
{
sprite.LayerSetVisible(Layers.ConnectedToPort, isConnected);
}
}
private enum Layers
{
ConnectedToPort,
}
}
}