Adds Gas Analyzer (#1591)

* -Started Gas Analyzer
-TemperatureHelpers

* Formatting

* Adds PopupTooltip to NotifyManager

* Revert Tooltip fuckery

* Gas Analyzer gives proper error messages

* Localization

* Added a very wip gas analyzer ui

* UI works, doesn't look good but hey

* Safety checks

* Fancy WIP gas mix bar

* Gas Color

* Gas Amount shows only 2 decimal places

* -Made bar full width
-Moved gas list into a table
-Some gas bar things

* IDropped something

* Description

* -Percentage
-Padding

* ItemStatus

* -Proper Danger Warnings
-Added Warning danger state

* Pressure unit

Co-authored-by: Víctor Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
Exp
2020-08-08 18:24:41 +02:00
committed by GitHub
parent 5b3b2e3207
commit cc9f16e738
14 changed files with 750 additions and 13 deletions

View File

@@ -0,0 +1,45 @@
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
using System;
using System.Collections.Generic;
using System.Text;
using static Content.Shared.GameObjects.Components.SharedGasAnalyzerComponent;
namespace Content.Client.GameObjects.Components.Atmos
{
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);
_menu.Close();
}
}
}

View File

@@ -0,0 +1,73 @@
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.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;
using System;
using System.Collections.Generic;
using System.Text;
namespace Content.Client.GameObjects.Components.Atmos
{
[RegisterComponent]
class GasAnalyzerComponent : SharedGasAnalyzerComponent, IItemStatus
{
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
[ViewVariables] public GasAnalyzerDanger Danger { get; private set; }
Control IItemStatus.MakeControl()
{
return new StatusControl(this);
}
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
if (!(curState is 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;
}
protected override void Update(FrameEventArgs args)
{
base.Update(args);
if (!_parent._uiUpdateNeeded)
{
return;
}
_parent._uiUpdateNeeded = false;
var color = _parent.Danger switch
{
GasAnalyzerDanger.Warning => "orange",
GasAnalyzerDanger.Hazard => "red",
_ => "green",
};
_label.SetMarkup(Loc.GetString("Pressure: [color={0}]{1}[/color]",
color,
Enum.GetName(typeof(GasAnalyzerDanger), _parent.Danger)));
}
}
}
}

View File

@@ -0,0 +1,284 @@
using System;
using Content.Client.Animations;
using Content.Client.GameObjects.EntitySystems;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Animations;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Content.Shared.GameObjects.Components.SharedGasAnalyzerComponent;
namespace Content.Client.GameObjects.Components.Atmos
{
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 {CustomMinimumSize = (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 MarginContainer
{
MarginLeftOverride = 4,
MarginTopOverride = 2,
MarginRightOverride = 12,
MarginBottomOverride = 2,
Children =
{
new HBoxContainer
{
Children =
{
(_nameLabel = new Label
{
Text = Loc.GetString("Gas Analyzer"),
FontOverride = font,
FontColorOverride = StyleNano.NanoGold,
SizeFlagsVertical = SizeFlags.ShrinkCenter
}),
new Control
{
CustomMinimumSize = (20, 0),
SizeFlagsHorizontal = SizeFlags.Expand
},
(refreshButton = new Button {Text = "Refresh"}), //TODO: refresh icon?
new Control
{
CustomMinimumSize = (2, 0),
},
(CloseButton = new TextureButton
{
StyleClasses = {SS14Window.StyleClassWindowCloseButton},
SizeFlagsVertical = SizeFlags.ShrinkCenter
})
}
}
}
};
refreshButton.OnPressed += a =>
{
Owner.Refresh();
};
var middle = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#202025") },
Children =
{
new MarginContainer
{
MarginLeftOverride = 8,
MarginRightOverride = 8,
MarginTopOverride = 4,
MarginBottomOverride = 4,
Children =
{
(_statusContainer = new VBoxContainer())
}
}
}
};
_topContainer.AddChild(topRow);
_topContainer.AddChild(new PanelContainer
{
CustomMinimumSize = (0, 2),
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") }
});
_topContainer.AddChild(middle);
_topContainer.AddChild(new PanelContainer
{
CustomMinimumSize = (0, 2),
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") }
});
CloseButton.OnPressed += _ => Close();
LayoutContainer.SetSize(this, (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.Pressure, TemperatureHelpers.KelvinToCelsius(state.Pressure))
});
// Return here cause all that stuff down there is gas stuff (so we don't get the seperators)
if (state.Gases.Length == 0)
{
return;
}
// Seperator
_statusContainer.AddChild(new Control
{
CustomMinimumSize = 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
{
CustomMinimumSize = 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
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
CustomMinimumSize = new Vector2(0, height)
};
// Seperator
_statusContainer.AddChild(new Control
{
CustomMinimumSize = 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),
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsStretchRatio = gas.Amount,
MouseFilter = MouseFilterMode.Pass,
PanelOverride = new StyleBoxFlat
{
BackgroundColor = color,
PaddingLeft = left,
PaddingRight = right
},
CustomMinimumSize = 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

@@ -1,39 +1,190 @@
#nullable enable
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using System;
using System.Collections.Generic;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasAnalyzerComponent : Component, IExamine
public class GasAnalyzerComponent : SharedGasAnalyzerComponent, IAfterInteract, IDropped
{
public override string Name => "GasAnalyzer";
public void Examine(FormattedMessage message, bool inDetailsRange)
{
if (!inDetailsRange) return;
#pragma warning disable 649
[Dependency] private IServerNotifyManager _notifyManager = default!;
#pragma warning restore 649
private BoundUserInterface _userInterface = default!;
private GasAnalyzerDanger _pressureDanger;
private float _timeSinceSync;
private const float TimeBetweenSyncs = 10f;
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetComponent<ServerUserInterfaceComponent>()
.GetBoundUserInterface(GasAnalyzerUiKey.Key);
_userInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
}
public override ComponentState GetComponentState()
{
return new GasAnalyzerComponentState(_pressureDanger);
}
/// <summary>
/// Call this from other components to open the gas analyzer UI.
/// </summary>
public void OpenInterface(IPlayerSession session)
{
_userInterface.Open(session);
UpdateUserInterface();
Resync();
}
public void CloseInterface(IPlayerSession session)
{
_userInterface.Close(session);
Resync();
}
public void Update(float frameTime)
{
_timeSinceSync += frameTime;
if (_timeSinceSync > TimeBetweenSyncs)
{
Resync();
}
}
private void Resync()
{
// Already get the pressure before Dirty(), because we can't get the EntitySystem in that thread or smth
var pressure = 0f;
var gam = EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(Owner.Transform.GridID);
var tile = gam?.GetTile(Owner.Transform.GridPosition).Air;
if (tile != null)
{
pressure = tile.Pressure;
}
if (pressure >= Atmospherics.HazardHighPressure || pressure <= Atmospherics.HazardLowPressure)
{
_pressureDanger = GasAnalyzerDanger.Hazard;
}
else if (pressure >= Atmospherics.WarningHighPressure || pressure <= Atmospherics.WarningLowPressure)
{
_pressureDanger = GasAnalyzerDanger.Warning;
}
else
{
_pressureDanger = GasAnalyzerDanger.Nominal;
}
Dirty();
_timeSinceSync = 0f;
}
private void UpdateUserInterface()
{
string? error = null;
var gam = EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(Owner.Transform.GridID);
var tile = gam?.GetTile(Owner.Transform.GridPosition).Air;
if (tile == null)
{
error = "No Atmosphere!";
_userInterface.SetState(
new GasAnalyzerBoundUserInterfaceState(
0,
0,
null,
error));
return;
}
if (tile == null) return;
message.AddText($"Pressure: {tile.Pressure}\n");
message.AddText($"Temperature: {tile.Temperature}\n");
var gases = new List<GasEntry>();
for (int i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var gas = Atmospherics.GetGas(i);
if (tile.Gases[i] <= Atmospherics.GasMinMoles) continue;
message.AddText(gas.Name);
message.AddText($"\n Moles: {tile.Gases[i]}\n");
gases.Add(new GasEntry(gas.Name, tile.Gases[i], gas.Color));
}
_userInterface.SetState(
new GasAnalyzerBoundUserInterfaceState(
tile.Pressure,
tile.Temperature,
gases.ToArray(),
error));
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
var message = serverMsg.Message;
switch (message)
{
case GasAnalyzerRefreshMessage msg:
var player = serverMsg.Session.AttachedEntity;
if (player == null)
{
return;
}
if (!player.TryGetComponent(out IHandsComponent handsComponent))
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, player,
Loc.GetString("You have no hands."));
return;
}
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
if (activeHandEntity == null || !activeHandEntity.TryGetComponent(out GasAnalyzerComponent gasAnalyzer))
{
_notifyManager.PopupMessage(serverMsg.Session.AttachedEntity,
serverMsg.Session.AttachedEntity,
Loc.GetString("You need a Gas Analyzer in your hand!"));
return;
}
UpdateUserInterface();
Resync();
break;
}
}
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out IActorComponent actor))
{
OpenInterface(actor.playerSession);
//TODO: show other sprite when ui open?
}
}
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
if (eventArgs.User.TryGetComponent(out IActorComponent actor))
{
CloseInterface(actor.playerSession);
//TODO: if other sprite is shown, change again
}
}
}

View File

@@ -0,0 +1,21 @@
using Content.Server.GameObjects.Components.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using System;
using System.Collections.Generic;
using System.Text;
namespace Content.Server.GameObjects.EntitySystems
{
public class GasAnalyzerSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var analyzer in ComponentManager.EntityQuery<GasAnalyzerComponent>())
{
analyzer.Update(frameTime);
}
}
}
}

View File

@@ -69,6 +69,8 @@ namespace Content.Shared.Atmos
/// </summary>
public string OverlayPath { get; private set; }
public string Color { get; private set; }
public void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
@@ -81,6 +83,7 @@ namespace Content.Shared.Atmos
serializer.DataField(this, x => GasOverlayTexture, "gasOverlayTexture", string.Empty);
serializer.DataField(this, x => GasOverlaySprite, "gasOverlaySprite", string.Empty);
serializer.DataField(this, x => GasOverlayState, "gasOverlayState", string.Empty);
serializer.DataField(this, x => Color, "color", string.Empty);
}
}
}

View File

@@ -0,0 +1,84 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.UserInterface;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using System;
using System.Collections.Generic;
using System.Text;
namespace Content.Shared.GameObjects.Components
{
public class SharedGasAnalyzerComponent : Component
{
public override string Name => "GasAnalyzer";
public override uint? NetID => ContentNetIDs.GAS_ANALYZER;
[Serializable, NetSerializable]
public enum GasAnalyzerUiKey
{
Key,
}
[Serializable, NetSerializable]
public class GasAnalyzerBoundUserInterfaceState : BoundUserInterfaceState
{
public float Pressure;
public float Temperature;
public GasEntry[] Gases;
public string Error;
public GasAnalyzerBoundUserInterfaceState(float pressure, float temperature, GasEntry[] gases, string error = null)
{
Pressure = pressure;
Temperature = temperature;
Gases = gases;
Error = error;
}
}
[Serializable, NetSerializable]
public struct GasEntry
{
public readonly string Name;
public readonly float Amount;
public readonly string Color;
public GasEntry(string name, float amount, string color)
{
Name = name;
Amount = amount;
Color = color;
}
public override string ToString()
{
return Loc.GetString("{0}: {1:0.##} mol", Name, Amount);
}
}
[Serializable, NetSerializable]
public class GasAnalyzerRefreshMessage : BoundUserInterfaceMessage
{
public GasAnalyzerRefreshMessage() {}
}
[Serializable, NetSerializable]
public enum GasAnalyzerDanger
{
Nominal,
Warning,
Hazard
}
[Serializable, NetSerializable]
public class GasAnalyzerComponentState : ComponentState
{
public GasAnalyzerDanger Danger;
public GasAnalyzerComponentState(GasAnalyzerDanger danger) : base(ContentNetIDs.GAS_ANALYZER)
{
Danger = danger;
}
}
}
}

View File

@@ -61,7 +61,8 @@
public const uint THROWN_ITEM = 1054;
public const uint STRAP = 1055;
public const uint DISPOSABLE = 1056;
public const uint DO_AFTER = 1057;
public const uint GAS_ANALYZER = 1057;
public const uint DO_AFTER = 1058;
// Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001;

View File

@@ -0,0 +1,20 @@
using Content.Shared.Maths;
using System;
using System.Collections.Generic;
using System.Text;
namespace Content.Shared.Utility
{
public static class TemperatureHelpers
{
public static float CelsiusToKelvin(float celsius)
{
return celsius + PhysicalConstants.ZERO_CELCIUS;
}
public static float KelvinToCelsius(float kelvin)
{
return kelvin - PhysicalConstants.ZERO_CELCIUS;
}
}
}

View File

@@ -2,16 +2,19 @@
id: 0
name: Oxygen
specificHeat: 20
color: 2887E8
- type: gas
id: 1
name: Nitrogen
specificHeat: 30
color: DA1010
- type: gas
id: 2
name: Carbon Dioxide
specificHeat: 30
color: 4e4e4e
- type: gas
id: 3
@@ -19,6 +22,7 @@
specificHeat: 200
gasOverlaySprite: /Textures/Effects/atmospherics.rsi
gasOverlayState: phoron
color: FF3300
- type: gas
id: 4
@@ -26,3 +30,4 @@
specificHeat: 10
gasOverlaySprite: /Textures/Effects/atmospherics.rsi
gasOverlayState: tritium
color: 13FF4B

View File

@@ -0,0 +1,17 @@
- type: entity
name: gas analyzer
parent: BaseItem
id: GasAnalyzer
description: A hand-held environmental scanner which reports current gas levels.
components:
- type: Sprite
sprite: Objects/Specific/Atmos/gasanalyzer.rsi
state: icon
- type: Icon
sprite: Objects/Specific/Atmos/gasanalyzer.rsi
state: icon
- type: GasAnalyzer
- type: UserInterface
interfaces:
- key: enum.GasAnalyzerUiKey.Key
type: GasAnalyzerBoundUserInterface

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,33 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon",
"directions": 1,
"delays": [
[
1.0
]
]
},
{
"name": "atmos2",
"directions": 1,
"delays": [
[
0.1,
0.1,
0.1,
3.0,
0.3,
0.2,
0.8
]
]
}
]
}