Crew monitor revisit (#22240)

This commit is contained in:
chromiumboy
2023-12-09 23:38:50 -06:00
committed by GitHub
parent ffb9112dc5
commit b70c0845d0
28 changed files with 1871 additions and 1302 deletions

View File

@@ -1,53 +1,56 @@
using Content.Shared.Medical.CrewMonitoring; using Content.Shared.Medical.CrewMonitoring;
using Robust.Client.GameObjects;
namespace Content.Client.Medical.CrewMonitoring namespace Content.Client.Medical.CrewMonitoring;
public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface
{ {
public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface [ViewVariables]
private CrewMonitoringWindow? _menu;
public CrewMonitoringBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{ {
[ViewVariables] }
private CrewMonitoringWindow? _menu;
public CrewMonitoringBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) protected override void Open()
{
EntityUid? gridUid = null;
string stationName = string.Empty;
if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
{ {
} gridUid = xform.GridUid;
protected override void Open() if (EntMan.TryGetComponent<MetaDataComponent>(gridUid, out var metaData))
{
EntityUid? gridUid = null;
if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
{ {
gridUid = xform.GridUid; stationName = metaData.EntityName;
}
_menu = new CrewMonitoringWindow(gridUid);
_menu.OpenCentered();
_menu.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
switch (state)
{
case CrewMonitoringState st:
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
_menu?.ShowSensors(st.Sensors, xform?.Coordinates, st.Snap, st.Precision);
break;
} }
} }
protected override void Dispose(bool disposing) _menu = new CrewMonitoringWindow(stationName, gridUid);
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Dispose(); _menu.OpenCentered();
_menu.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
switch (state)
{
case CrewMonitoringState st:
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
_menu?.ShowSensors(st.Sensors, Owner, xform?.Coordinates);
break;
} }
} }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Dispose();
}
} }

View File

@@ -0,0 +1,79 @@
using Content.Client.Pinpointer.UI;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Medical.CrewMonitoring;
public sealed partial class CrewMonitoringNavMapControl : NavMapControl
{
public NetEntity? Focus;
public Dictionary<NetEntity, string> LocalizedNames = new();
private Color _backgroundColor;
private Label _trackedEntityLabel;
private PanelContainer _trackedEntityPanel;
public CrewMonitoringNavMapControl() : base()
{
WallColor = new Color(250, 146, 255);
TileColor = new(71, 42, 72);
_backgroundColor = Color.FromSrgb(TileColor.WithAlpha(0.8f));
_trackedEntityLabel = new Label
{
Margin = new Thickness(10f, 8f),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Modulate = Color.White,
};
_trackedEntityPanel = new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = _backgroundColor,
},
Margin = new Thickness(5f, 10f),
HorizontalAlignment = HAlignment.Left,
VerticalAlignment = VAlignment.Bottom,
Visible = false,
};
_trackedEntityPanel.AddChild(_trackedEntityLabel);
this.AddChild(_trackedEntityPanel);
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (Focus == null)
{
_trackedEntityLabel.Text = string.Empty;
_trackedEntityPanel.Visible = false;
return;
}
foreach ((var netEntity, var blip) in TrackedEntities)
{
if (netEntity != Focus)
continue;
if (!LocalizedNames.TryGetValue(netEntity, out var name))
name = "Unknown";
var message = name + "\nLocation: [x = " + MathF.Round(blip.Coordinates.X) + ", y = " + MathF.Round(blip.Coordinates.Y) + "]";
_trackedEntityLabel.Text = message;
_trackedEntityPanel.Visible = true;
return;
}
_trackedEntityLabel.Text = string.Empty;
_trackedEntityPanel.Visible = false;
}
}

View File

@@ -1,39 +1,49 @@
<controls:FancyWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI" xmlns:ui="clr-namespace:Content.Client.Medical.CrewMonitoring"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'crew-monitoring-user-interface-title'}" Title="{Loc 'crew-monitoring-user-interface-title'}"
SetSize="1130 700" SetSize="1200 700"
MinSize="1130 700"> MinSize="1200 700">
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Vertical">
<ScrollContainer HorizontalExpand="True" <BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
VerticalExpand="True" <ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20"/>
Margin="8, 8, 8, 8"> <BoxContainer Orientation="Vertical">
<GridContainer Name="SensorsTable" <controls:StripeBack>
HorizontalExpand="True" <PanelContainer>
VerticalExpand="True" <Label Name="StationName" Text="Unknown station" Align="Center" />
HSeparationOverride="5" </PanelContainer>
VSeparationOverride="20" </controls:StripeBack>
Columns="4">
<!-- Table header -->
<Label Text="{Loc 'crew-monitoring-user-interface-name'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-job'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-status'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-location'}"
StyleClasses="LabelHeading"/>
<!-- Table rows are filled by code --> <ScrollContainer Name="SensorScroller"
</GridContainer> VerticalExpand="True"
<Label Name="NoServerLabel" SetWidth="520"
Text="{Loc 'crew-monitoring-user-interface-no-server'}" Margin="8, 8, 8, 8">
StyleClasses="LabelHeading" <BoxContainer Name="SensorsTable"
FontColorOverride="Red" Orientation="Vertical"
HorizontalAlignment="Center" HorizontalExpand="True"
Visible="false"/> Margin="0 0 10 0">
</ScrollContainer> <!-- Table rows are filled by code -->
<ui:NavMapControl Name="NavMap" </BoxContainer>
Margin="5 5"/> <Label Name="NoServerLabel"
Text="{Loc 'crew-monitoring-user-interface-no-server'}"
StyleClasses="LabelHeading"
FontColorOverride="Red"
HorizontalAlignment="Center"
Visible="false"/>
</ScrollContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'crew-monitoring-user-interface-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'crew-monitoring-user-interface-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer> </BoxContainer>
</controls:FancyWindow> </controls:FancyWindow>

View File

@@ -1,275 +1,437 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Client.Pinpointer.UI;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Content.Shared.StatusIcon;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer; using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Medical.CrewMonitoring namespace Content.Client.Medical.CrewMonitoring;
[GenerateTypedNameReferences]
public sealed partial class CrewMonitoringWindow : FancyWindow
{ {
[GenerateTypedNameReferences] private List<Control> _rowsContent = new();
public sealed partial class CrewMonitoringWindow : FancyWindow private readonly IEntityManager _entManager;
private readonly IPrototypeManager _prototypeManager;
private readonly SpriteSystem _spriteSystem;
private NetEntity? _trackedEntity;
private bool _tryToScrollToListFocus;
private Texture? _blipTexture;
public CrewMonitoringWindow(string stationName, EntityUid? mapUid)
{ {
private List<Control> _rowsContent = new(); RobustXamlLoader.Load(this);
private List<(DirectionIcon Icon, Vector2 Position)> _directionIcons = new();
private readonly IEntityManager _entManager;
private readonly IEyeManager _eye;
private EntityUid? _stationUid;
private CrewMonitoringButton? _trackedButton;
public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row. _entManager = IoCManager.Resolve<IEntityManager>();
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
_spriteSystem = _entManager.System<SpriteSystem>();
public CrewMonitoringWindow(EntityUid? mapUid) _blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
if (_entManager.TryGetComponent<TransformComponent>(mapUid, out var xform))
NavMap.MapUid = xform.GridUid;
else
NavMap.Visible = false;
StationName.AddStyleClass("LabelBig");
StationName.Text = stationName;
NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
NavMap.ForceNavMapUpdate();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_tryToScrollToListFocus)
TryToScrollToFocus();
}
public void ShowSensors(List<SuitSensorStatus> sensors, EntityUid monitor, EntityCoordinates? monitorCoords)
{
ClearOutDatedData();
// No server label
if (sensors.Count == 0)
{ {
RobustXamlLoader.Load(this); NoServerLabel.Visible = true;
_eye = IoCManager.Resolve<IEyeManager>(); return;
_entManager = IoCManager.Resolve<IEntityManager>();
_stationUid = mapUid;
if (_entManager.TryGetComponent<TransformComponent>(mapUid, out var xform))
{
NavMap.MapUid = xform.GridUid;
}
else
{
NavMap.Visible = false;
SetSize = new Vector2(775, 400);
MinSize = SetSize;
}
} }
public void ShowSensors(List<SuitSensorStatus> stSensors, EntityCoordinates? monitorCoords, bool snap, float precision) NoServerLabel.Visible = false;
// Order sensor data
var orderedSensors = sensors.OrderBy(n => n.Name).OrderBy(j => j.Job);
var assignedSensors = new HashSet<SuitSensorStatus>();
var departments = sensors.SelectMany(d => d.JobDepartments).Distinct().OrderBy(n => n);
// Create department labels and populate lists
foreach (var department in departments)
{ {
ClearAllSensors(); var departmentSensors = orderedSensors.Where(d => d.JobDepartments.Contains(department));
var monitorCoordsInStationSpace = _stationUid != null ? monitorCoords?.WithEntityId(_stationUid.Value, _entManager).Position : null; if (departmentSensors == null || !departmentSensors.Any())
continue;
// TODO scroll container foreach (var sensor in departmentSensors)
// TODO filter by name & occupation assignedSensors.Add(sensor);
// TODO make each row a xaml-control. Get rid of some of this c# control creation.
if (stSensors.Count == 0) if (SensorsTable.ChildCount > 0)
{ {
NoServerLabel.Visible = true; var spacer = new Control()
return;
}
NoServerLabel.Visible = false;
// add a row for each sensor
foreach (var sensor in stSensors.OrderBy(a => a.Name))
{
var sensorEntity = _entManager.GetEntity(sensor.SuitSensorUid);
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
// add button with username
var nameButton = new CrewMonitoringButton()
{ {
SuitSensorUid = sensorEntity, SetHeight = 20,
Coordinates = coordinates,
Text = sensor.Name,
Margin = new Thickness(5f, 5f),
}; };
if (sensorEntity == _trackedButton?.SuitSensorUid)
nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
SetColorLabel(nameButton.Label, sensor.TotalDamage, sensor.IsAlive);
SensorsTable.AddChild(nameButton);
_rowsContent.Add(nameButton);
// add users job SensorsTable.AddChild(spacer);
// format: JobName _rowsContent.Add(spacer);
var jobLabel = new Label()
{
Text = sensor.Job,
HorizontalExpand = true
};
SetColorLabel(jobLabel, sensor.TotalDamage, sensor.IsAlive);
SensorsTable.AddChild(jobLabel);
_rowsContent.Add(jobLabel);
// add users status and damage
// format: IsAlive (TotalDamage)
var statusText = Loc.GetString(sensor.IsAlive ?
"crew-monitoring-user-interface-alive" :
"crew-monitoring-user-interface-dead");
if (sensor.TotalDamage != null)
{
statusText += $" ({sensor.TotalDamage})";
}
var statusLabel = new Label()
{
Text = statusText
};
SetColorLabel(statusLabel, sensor.TotalDamage, sensor.IsAlive);
SensorsTable.AddChild(statusLabel);
_rowsContent.Add(statusLabel);
// add users positions
// format: (x, y)
var box = GetPositionBox(sensor, monitorCoordsInStationSpace ?? Vector2.Zero, snap, precision);
SensorsTable.AddChild(box);
_rowsContent.Add(box);
if (coordinates != null && NavMap.Visible)
{
NavMap.TrackedCoordinates.TryAdd(coordinates.Value,
(true, sensorEntity == _trackedButton?.SuitSensorUid ? StyleNano.PointGreen : StyleNano.PointRed));
nameButton.OnButtonUp += args =>
{
if (_trackedButton != null && _trackedButton?.Coordinates != null)
//Make previous point red
NavMap.TrackedCoordinates[_trackedButton.Coordinates.Value] = (true, StyleNano.PointRed);
NavMap.TrackedCoordinates[coordinates.Value] = (true, StyleNano.PointGreen);
NavMap.CenterToCoordinates(coordinates.Value);
nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
if (_trackedButton != null)
{ //Make previous button default
var previosButton = SensorsTable.GetChild(_trackedButton.IndexInTable);
previosButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
}
_trackedButton = nameButton;
_trackedButton.IndexInTable = nameButton.GetPositionInParent();
};
}
} }
// Show monitor point
if (monitorCoords != null) var deparmentLabel = new RichTextLabel()
NavMap.TrackedCoordinates.Add(monitorCoords.Value, (true, StyleNano.PointMagenta)); {
Margin = new Thickness(10, 0),
HorizontalExpand = true,
};
deparmentLabel.SetMessage(department);
deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
SensorsTable.AddChild(deparmentLabel);
_rowsContent.Add(deparmentLabel);
PopulateDepartmentList(departmentSensors);
} }
private BoxContainer GetPositionBox(SuitSensorStatus sensor, Vector2 monitorCoordsInStationSpace, bool snap, float precision) // Account for any non-station users
var remainingSensors = orderedSensors.Except(assignedSensors);
if (remainingSensors.Any())
{ {
EntityCoordinates? coordinates = _entManager.GetCoordinates(sensor.Coordinates); var spacer = new Control()
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
if (coordinates == null || _stationUid == null)
{ {
var dirIcon = new DirectionIcon() SetHeight = 20,
{ };
SetSize = new Vector2(IconSize, IconSize),
Margin = new(0, 0, 4, 0) SensorsTable.AddChild(spacer);
}; _rowsContent.Add(spacer);
box.AddChild(dirIcon);
box.AddChild(new Label() { Text = Loc.GetString("crew-monitoring-user-interface-no-info") }); var deparmentLabel = new RichTextLabel()
}
else
{ {
var local = coordinates.Value.WithEntityId(_stationUid.Value, _entManager).Position; Margin = new Thickness(10, 0),
HorizontalExpand = true,
};
var displayPos = local.Floored(); deparmentLabel.SetMessage(Loc.GetString("crew-monitoring-user-interface-no-department"));
var dirIcon = new DirectionIcon(snap, precision) deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
{
SetSize = new Vector2(IconSize, IconSize),
Margin = new(0, 0, 4, 0)
};
box.AddChild(dirIcon);
Label label = new Label() { Text = displayPos.ToString() };
SetColorLabel(label, sensor.TotalDamage, sensor.IsAlive);
box.AddChild(label);
_directionIcons.Add((dirIcon, local - monitorCoordsInStationSpace));
}
return box; SensorsTable.AddChild(deparmentLabel);
_rowsContent.Add(deparmentLabel);
PopulateDepartmentList(remainingSensors);
} }
protected override void FrameUpdate(FrameEventArgs args) // Show monitor on nav map
if (monitorCoords != null && _blipTexture != null)
{ {
// the window is separate from any specific viewport, so there is no real way to get an eye-rotation without NavMap.TrackedEntities[_entManager.GetNetEntity(monitor)] = new NavMapBlip(monitorCoords.Value, _blipTexture, Color.Cyan, true, false);
// using IEyeManager. Eventually this will have to be reworked for a station AI with multi-viewports.
// (From the future: Or alternatively, just disable the angular offset for station AIs?)
// An offsetAngle of zero here perfectly aligns directions to the station map.
// Note that the "relative angle" does this weird inverse-inverse thing.
// Could recalculate it all in world coordinates and then pass in eye directly... or do this.
var offsetAngle = Angle.Zero;
if (_entManager.TryGetComponent<TransformComponent>(_stationUid, out var xform))
{
// Apply the offset relative to the eye.
// For a station at 45 degrees rotation, the current eye rotation is -45 degrees.
// TODO: This feels sketchy. Is there something underlying wrong with eye rotation?
offsetAngle = -(_eye.CurrentEye.Rotation + xform.WorldRotation);
}
foreach (var (icon, pos) in _directionIcons)
{
icon.UpdateDirection(pos, offsetAngle);
}
}
private void ClearAllSensors()
{
foreach (var child in _rowsContent)
{
SensorsTable.RemoveChild(child);
}
_rowsContent.Clear();
_directionIcons.Clear();
NavMap.TrackedCoordinates.Clear();
}
private void SetColorLabel(Label label, int? totalDamage, bool isAlive)
{
var startColor = Color.White;
var critColor = Color.Yellow;
var endColor = Color.Red;
if (!isAlive)
{
label.FontColorOverride = endColor;
return;
}
//Convert from null to regular int
int damage;
if (totalDamage == null) return;
else damage = (int) totalDamage;
if (damage <= 0)
{
label.FontColorOverride = startColor;
}
else if (damage >= 200)
{
label.FontColorOverride = endColor;
}
else if (damage >= 0 && damage <= 100)
{
label.FontColorOverride = GetColorLerp(startColor, critColor, damage);
}
else if (damage >= 100 && damage <= 200)
{
//We need a number from 0 to 100. Divide the number from 100 to 200 by 2
damage /= 2;
label.FontColorOverride = GetColorLerp(critColor, endColor, damage);
}
}
private Color GetColorLerp(Color startColor, Color endColor, int damage)
{
//Smooth transition from one color to another depending on the percentage
var t = damage / 100f;
var r = MathHelper.Lerp(startColor.R, endColor.R, t);
var g = MathHelper.Lerp(startColor.G, endColor.G, t);
var b = MathHelper.Lerp(startColor.B, endColor.B, t);
var a = MathHelper.Lerp(startColor.A, endColor.A, t);
return new Color(r, g, b, a);
} }
} }
public sealed class CrewMonitoringButton : Button private void PopulateDepartmentList(IEnumerable<SuitSensorStatus> departmentSensors)
{ {
public int IndexInTable; // Populate departments
public EntityUid? SuitSensorUid; foreach (var sensor in departmentSensors)
public EntityCoordinates? Coordinates; {
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
// Add a button that will hold a username and other details
NavMap.LocalizedNames.TryAdd(sensor.SuitSensorUid, sensor.Name + ", " + sensor.Job);
var sensorButton = new CrewMonitoringButton()
{
SuitSensorUid = sensor.SuitSensorUid,
Coordinates = coordinates,
Disabled = (coordinates == null),
HorizontalExpand = true,
};
if (sensor.SuitSensorUid == _trackedEntity)
sensorButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
SensorsTable.AddChild(sensorButton);
_rowsContent.Add(sensorButton);
// Primary container to hold the button UI elements
var mainContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
sensorButton.AddChild(mainContainer);
// User status container
var statusContainer = new BoxContainer()
{
SizeFlagsStretchRatio = 1.25f,
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
mainContainer.AddChild(statusContainer);
// Suit coords indicator
var suitCoordsIndicator = new TextureRect()
{
Texture = _blipTexture,
TextureScale = new Vector2(0.25f, 0.25f),
Modulate = coordinates != null ? Color.LimeGreen : Color.DarkRed,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
};
statusContainer.AddChild(suitCoordsIndicator);
// Specify texture for the user status icon
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "alive");
if (!sensor.IsAlive)
{
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "dead");
}
else if (sensor.TotalDamage != null)
{
var index = MathF.Round(4f * (sensor.TotalDamage.Value / 100f));
if (index >= 5)
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical");
else
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "health" + index);
}
// Status icon
var statusIcon = new AnimatedTextureRect
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(0, 1, 3, 0),
};
statusIcon.SetFromSpriteSpecifier(specifier);
statusIcon.DisplayRect.TextureScale = new Vector2(2f, 2f);
statusContainer.AddChild(statusIcon);
// User name
var nameLabel = new Label()
{
Text = sensor.Name,
HorizontalExpand = true,
ClipText = true,
};
statusContainer.AddChild(nameLabel);
// User job container
var jobContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
mainContainer.AddChild(jobContainer);
// Job icon
if (_prototypeManager.TryIndex<StatusIconPrototype>(sensor.JobIcon, out var proto))
{
var jobIcon = new TextureRect()
{
TextureScale = new Vector2(2f, 2f),
Stretch = TextureRect.StretchMode.KeepCentered,
Texture = _spriteSystem.Frame0(proto.Icon),
Margin = new Thickness(5, 0, 5, 0),
};
jobContainer.AddChild(jobIcon);
}
// Job name
var jobLabel = new Label()
{
Text = sensor.Job,
HorizontalExpand = true,
ClipText = true,
};
jobContainer.AddChild(jobLabel);
// Add user coordinates to the navmap
if (coordinates != null && NavMap.Visible && _blipTexture != null)
{
NavMap.TrackedEntities.TryAdd(sensor.SuitSensorUid,
new NavMapBlip
(coordinates.Value,
_blipTexture,
(_trackedEntity == null || sensor.SuitSensorUid == _trackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
sensor.SuitSensorUid == _trackedEntity));
NavMap.Focus = _trackedEntity;
// On button up
sensorButton.OnButtonUp += args =>
{
var prevTrackedEntity = _trackedEntity;
if (_trackedEntity == sensor.SuitSensorUid)
{
_trackedEntity = null;
}
else
{
_trackedEntity = sensor.SuitSensorUid;
NavMap.CenterToCoordinates(coordinates.Value);
}
NavMap.Focus = _trackedEntity;
UpdateSensorsTable(_trackedEntity, prevTrackedEntity);
};
}
}
}
private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
{
var prevTrackedEntity = _trackedEntity;
_trackedEntity = netEntity;
if (_trackedEntity == prevTrackedEntity)
prevTrackedEntity = null;
NavMap.Focus = _trackedEntity;
_tryToScrollToListFocus = true;
UpdateSensorsTable(_trackedEntity, prevTrackedEntity);
}
private void UpdateSensorsTable(NetEntity? currTrackedEntity, NetEntity? prevTrackedEntity)
{
foreach (var sensor in SensorsTable.Children)
{
if (sensor is not CrewMonitoringButton)
continue;
var castSensor = (CrewMonitoringButton) sensor;
if (castSensor.SuitSensorUid == prevTrackedEntity)
castSensor.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
else if (castSensor.SuitSensorUid == currTrackedEntity)
castSensor.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
if (castSensor?.Coordinates == null)
continue;
if (NavMap.TrackedEntities.TryGetValue(castSensor.SuitSensorUid, out var data))
{
data = new NavMapBlip
(data.Coordinates,
data.Texture,
(currTrackedEntity == null || castSensor.SuitSensorUid == currTrackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
castSensor.SuitSensorUid == currTrackedEntity);
NavMap.TrackedEntities[castSensor.SuitSensorUid] = data;
}
}
}
private void TryToScrollToFocus()
{
if (!_tryToScrollToListFocus)
return;
if (!TryGetVerticalScrollbar(SensorScroller, out var vScrollbar))
return;
if (TryGetNextScrollPosition(out float? nextScrollPosition))
{
vScrollbar.ValueTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
{
_tryToScrollToListFocus = false;
return;
}
}
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var child in scroll.Children)
{
if (child is not VScrollBar)
continue;
vScrollBar = (VScrollBar) child;
return true;
}
return false;
}
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
{
nextScrollPosition = 0;
foreach (var sensor in SensorsTable.Children)
{
if (sensor is CrewMonitoringButton &&
((CrewMonitoringButton) sensor).SuitSensorUid == _trackedEntity)
return true;
nextScrollPosition += sensor.Height;
}
// Failed to find control
nextScrollPosition = null;
return false;
}
private void ClearOutDatedData()
{
SensorsTable.RemoveAllChildren();
_rowsContent.Clear();
NavMap.TrackedCoordinates.Clear();
NavMap.TrackedEntities.Clear();
NavMap.LocalizedNames.Clear();
} }
} }
public sealed class CrewMonitoringButton : Button
{
public int IndexInTable;
public NetEntity SuitSensorUid;
public EntityCoordinates? Coordinates;
}

View File

@@ -1,40 +1,66 @@
using System.Numerics;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Shared.Pinpointer; using Content.Shared.Pinpointer;
using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Collections;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using System.Numerics;
using JetBrains.Annotations;
namespace Content.Client.Pinpointer.UI; namespace Content.Client.Pinpointer.UI;
/// <summary> /// <summary>
/// Displays the nav map data of the specified grid. /// Displays the nav map data of the specified grid.
/// </summary> /// </summary>
public sealed class NavMapControl : MapGridControl [UsedImplicitly, Virtual]
public partial class NavMapControl : MapGridControl
{ {
[Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IEntityManager _entManager = default!;
private SharedTransformSystem _transform; private readonly SharedTransformSystem _transformSystem = default!;
public EntityUid? MapUid; public EntityUid? MapUid;
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new(); // Actions
public event Action<NetEntity?>? TrackedEntitySelectedAction;
// Tracked data
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
public Dictionary<NetEntity, NavMapBlip> TrackedEntities = new();
public Dictionary<Vector2i, List<NavMapLine>> TileGrid = default!;
// Default colors
public Color WallColor = new(102, 217, 102);
public Color TileColor = new(30, 67, 30);
// Constants
protected float UpdateTime = 1.0f;
protected float MaxSelectableDistance = 10f;
protected float RecenterMinimum = 0.05f;
// Local variables
private Vector2 _offset; private Vector2 _offset;
private bool _draggin; private bool _draggin;
private bool _recentering = false; private bool _recentering = false;
private readonly float _recenterMinimum = 0.05f;
private readonly Font _font; private readonly Font _font;
private static readonly Color TileColor = new(30, 67, 30); private float _updateTimer = 0.25f;
private static readonly Color BeaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f)); private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
private Color _beaconColor;
// Components
private NavMapComponent? _navMap;
private MapGridComponent? _grid;
private TransformComponent? _xform;
private PhysicsComponent? _physics;
private FixturesComponent? _fixtures;
// TODO: https://github.com/space-wizards/RobustToolbox/issues/3818 // TODO: https://github.com/space-wizards/RobustToolbox/issues/3818
private readonly Label _zoom = new() private readonly Label _zoom = new()
@@ -45,20 +71,30 @@ public sealed class NavMapControl : MapGridControl
private readonly Button _recenter = new() private readonly Button _recenter = new()
{ {
Text = "Recentre", Text = Loc.GetString("navmap-recenter"),
VerticalAlignment = VAlignment.Top, VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right, HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(8f, 4f), Margin = new Thickness(8f, 4f),
Disabled = true, Disabled = true,
}; };
private readonly CheckBox _beacons = new()
{
Text = Loc.GetString("navmap-toggle-beacons"),
Margin = new Thickness(4f, 0f),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Pressed = false,
};
public NavMapControl() : base(8f, 128f, 48f) public NavMapControl() : base(8f, 128f, 48f)
{ {
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_transform = _entManager.System<SharedTransformSystem>();
var cache = IoCManager.Resolve<IResourceCache>(); var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 16);
_transformSystem = _entManager.System<SharedTransformSystem>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 12);
_beaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f));
RectClipContent = true; RectClipContent = true;
HorizontalExpand = true; HorizontalExpand = true;
@@ -75,6 +111,7 @@ public sealed class NavMapControl : MapGridControl
Children = Children =
{ {
_zoom, _zoom,
_beacons,
_recenter, _recenter,
} }
}; };
@@ -101,14 +138,28 @@ public sealed class NavMapControl : MapGridControl
{ {
_recentering = true; _recentering = true;
}; };
ForceNavMapUpdate();
}
public void ForceRecenter()
{
_recentering = true;
}
public void ForceNavMapUpdate()
{
_entManager.TryGetComponent(MapUid, out _navMap);
_entManager.TryGetComponent(MapUid, out _grid);
UpdateNavMap();
} }
public void CenterToCoordinates(EntityCoordinates coordinates) public void CenterToCoordinates(EntityCoordinates coordinates)
{ {
if (_entManager.TryGetComponent<PhysicsComponent>(MapUid, out var physics)) if (_physics != null)
{ _offset = new Vector2(coordinates.X, coordinates.Y) - _physics.LocalCenter;
_offset = new Vector2(coordinates.X, coordinates.Y) - physics.LocalCenter;
}
_recenter.Disabled = false; _recenter.Disabled = false;
} }
@@ -117,18 +168,62 @@ public sealed class NavMapControl : MapGridControl
base.KeyBindDown(args); base.KeyBindDown(args);
if (args.Function == EngineKeyFunctions.Use) if (args.Function == EngineKeyFunctions.Use)
{
_draggin = true; _draggin = true;
}
} }
protected override void KeyBindUp(GUIBoundKeyEventArgs args) protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{ {
base.KeyBindUp(args); base.KeyBindUp(args);
if (TrackedEntitySelectedAction == null)
return;
if (args.Function == EngineKeyFunctions.Use) if (args.Function == EngineKeyFunctions.Use)
{ {
_draggin = false; _draggin = false;
if (_xform == null || _physics == null || TrackedEntities.Count == 0)
return;
// Get the clicked position
var offset = _offset + _physics.LocalCenter;
var localPosition = args.PointerLocation.Position - GlobalPixelPosition;
// Convert to a world position
var unscaledPosition = (localPosition - MidpointVector) / MinimapScale;
var worldPosition = _transformSystem.GetWorldMatrix(_xform).Transform(new Vector2(unscaledPosition.X, -unscaledPosition.Y) + offset);
// Find closest tracked entity in range
var closestEntity = NetEntity.Invalid;
var closestCoords = new EntityCoordinates();
var closestDistance = float.PositiveInfinity;
foreach ((var currentEntity, var blip) in TrackedEntities)
{
if (!blip.Selectable)
continue;
var currentDistance = (blip.Coordinates.ToMapPos(_entManager, _transformSystem) - worldPosition).Length();
if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance)
continue;
closestEntity = currentEntity;
closestCoords = blip.Coordinates;
closestDistance = currentDistance;
}
if (closestDistance > MaxSelectableDistance || !closestEntity.IsValid())
return;
TrackedEntitySelectedAction.Invoke(closestEntity);
}
else if (args.Function == EngineKeyFunctions.UIRightClick)
{
// Clear current selection with right click
if (TrackedEntitySelectedAction != null)
TrackedEntitySelectedAction.Invoke(null);
} }
} }
@@ -143,25 +238,30 @@ public sealed class NavMapControl : MapGridControl
_offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange; _offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange;
if (_offset != Vector2.Zero) if (_offset != Vector2.Zero)
{
_recenter.Disabled = false; _recenter.Disabled = false;
}
else else
{
_recenter.Disabled = true; _recenter.Disabled = true;
}
} }
protected override void Draw(DrawingHandleScreen handle) protected override void Draw(DrawingHandleScreen handle)
{ {
base.Draw(handle); base.Draw(handle);
// Get the components necessary for drawing the navmap
_entManager.TryGetComponent(MapUid, out _navMap);
_entManager.TryGetComponent(MapUid, out _grid);
_entManager.TryGetComponent(MapUid, out _xform);
_entManager.TryGetComponent(MapUid, out _physics);
_entManager.TryGetComponent(MapUid, out _fixtures);
// Map re-centering
if (_recentering) if (_recentering)
{ {
var frameTime = Timing.FrameTime; var frameTime = Timing.FrameTime;
var diff = _offset * (float) frameTime.TotalSeconds; var diff = _offset * (float) frameTime.TotalSeconds;
if (_offset.LengthSquared() < _recenterMinimum) if (_offset.LengthSquared() < RecenterMinimum)
{ {
_offset = Vector2.Zero; _offset = Vector2.Zero;
_recentering = false; _recentering = false;
@@ -173,29 +273,22 @@ public sealed class NavMapControl : MapGridControl
} }
} }
_zoom.Text = $"Zoom: {(WorldRange / WorldMaxRange * 100f):0.00}%"; _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(WorldRange / WorldMaxRange * 100f):0.00}"));
if (!_entManager.TryGetComponent<NavMapComponent>(MapUid, out var navMap) || if (_navMap == null || _xform == null)
!_entManager.TryGetComponent<TransformComponent>(MapUid, out var xform) ||
!_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
{
return; return;
}
var offset = _offset; var offset = _offset;
var lineColor = new Color(102, 217, 102);
if (_entManager.TryGetComponent<PhysicsComponent>(MapUid, out var physics)) if (_physics != null)
{ offset += _physics.LocalCenter;
offset += physics.LocalCenter;
}
// Draw tiles // Draw tiles
if (_entManager.TryGetComponent<FixturesComponent>(MapUid, out var manager)) if (_fixtures != null)
{ {
Span<Vector2> verts = new Vector2[8]; Span<Vector2> verts = new Vector2[8];
foreach (var fixture in manager.Fixtures.Values) foreach (var fixture in _fixtures.Fixtures.Values)
{ {
if (fixture.Shape is not PolygonShape poly) if (fixture.Shape is not PolygonShape poly)
continue; continue;
@@ -211,157 +304,305 @@ public sealed class NavMapControl : MapGridControl
} }
} }
// Draw the wall data
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset); var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
var tileSize = new Vector2(grid.TileSize, -grid.TileSize);
for (var x = Math.Floor(area.Left); x <= Math.Ceiling(area.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize) // Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order
// to figure out where they should be drawn. However, we don't *need* to do check these every frame.
// Instead, lets periodically update where to draw each line and then store these points in a list.
// Then we can just run through the list each frame and draw the lines without any extra computation.
// Draw walls
if (TileGrid != null && TileGrid.Count > 0)
{ {
for (var y = Math.Floor(area.Bottom); y <= Math.Ceiling(area.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize) var walls = new ValueList<Vector2>();
foreach ((var chunk, var chunkedLines) in TileGrid)
{ {
var floored = new Vector2i((int) x, (int) y); var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize); if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
continue; continue;
// TODO: Okay maybe I should just use ushorts lmao... if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++) continue;
foreach (var chunkedLine in chunkedLines)
{ {
var value = (int) Math.Pow(2, i); var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
var mask = chunk.TileData & value; walls.Add(start);
walls.Add(end);
if (mask == 0x0)
continue;
// Alright now we'll work out our edges
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize - offset;
var position = new Vector2(tile.X, -tile.Y);
NavMapChunk? neighborChunk;
bool neighbor;
// North edge
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + tileSize), lineColor);
}
// East edge
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position + tileSize), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
}
// South edge
if (relativeTile.Y == 0)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position + new Vector2(grid.TileSize, 0f)), Scale(position), lineColor);
}
// West edge
if (relativeTile.X == 0)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position), Scale(position + new Vector2(0f, -grid.TileSize)), lineColor);
}
// Draw a diagonal line for interiors.
handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
} }
} }
if (walls.Count > 0)
{
if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
{
sRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB);
}
}
// Beacons
if (_beacons.Pressed)
{
var rectBuffer = new Vector2(5f, 3f);
foreach (var beacon in _navMap.Beacons)
{
var position = beacon.Position - offset;
position = Scale(position with { Y = -position.Y });
var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f);
handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), _beaconColor);
handle.DrawString(_font, position - textDimensions / 2, beacon.Text, beacon.Color);
}
} }
var curTime = Timing.RealTime; var curTime = Timing.RealTime;
var blinkFrequency = 1f / 1f; var blinkFrequency = 1f / 1f;
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f; var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
// Tracked coordinates (simple dot, legacy)
foreach (var (coord, value) in TrackedCoordinates) foreach (var (coord, value) in TrackedCoordinates)
{ {
if (lit && value.Visible) if (lit && value.Visible)
{ {
var mapPos = coord.ToMap(_entManager); var mapPos = coord.ToMap(_entManager, _transformSystem);
if (mapPos.MapId != MapId.Nullspace) if (mapPos.MapId != MapId.Nullspace)
{ {
var position = xform.InvWorldMatrix.Transform(mapPos.Position) - offset; var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
position = Scale(new Vector2(position.X, -position.Y)); position = Scale(new Vector2(position.X, -position.Y));
handle.DrawCircle(position, MinimapScale / 2f, value.Color); handle.DrawCircle(position, float.Sqrt(MinimapScale) * 2f, value.Color);
} }
} }
} }
// Beacons // Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually)
var labelOffset = new Vector2(0.5f, 0.5f) * MinimapScale; var iconVertexUVs = new Dictionary<(Texture, Color), ValueList<DrawVertexUV2D>>();
var rectBuffer = new Vector2(5f, 3f);
foreach (var beacon in navMap.Beacons) foreach (var blip in TrackedEntities.Values)
{ {
var position = beacon.Position - offset; if (blip.Blinks && !lit)
continue;
position = Scale(position with { Y = -position.Y }); if (blip.Texture == null)
continue;
handle.DrawCircle(position, MinimapScale / 2f, beacon.Color); if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f); vertexUVs = new();
var labelPosition = position + labelOffset; var mapPos = blip.Coordinates.ToMap(_entManager, _transformSystem);
handle.DrawRect(new UIBox2(labelPosition, labelPosition + textDimensions + rectBuffer * 2), BeaconColor);
handle.DrawString(_font, labelPosition + rectBuffer, beacon.Text, beacon.Color); if (mapPos.MapId != MapId.Nullspace)
{
var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
position = Scale(new Vector2(position.X, -position.Y));
var scalingCoefficient = 2.5f;
var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f)));
}
iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs;
}
foreach ((var (texture, color), var vertexUVs) in iconVertexUVs)
{
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
{
sRGB = Color.ToSrgb(color);
_sRGBLookUp[color] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB);
} }
} }
private Vector2 Scale(Vector2 position) protected override void FrameUpdate(FrameEventArgs args)
{
// Update the timer
_updateTimer += args.DeltaSeconds;
if (_updateTimer >= UpdateTime)
{
_updateTimer -= UpdateTime;
UpdateNavMap();
}
}
private void UpdateNavMap()
{
if (_navMap == null || _grid == null)
return;
TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid);
}
public Dictionary<Vector2i, List<NavMapLine>> GetDecodedWallChunks
(Dictionary<Vector2i, NavMapChunk> chunks,
MapGridComponent grid)
{
var decodedOutput = new Dictionary<Vector2i, List<NavMapLine>>();
foreach ((var chunkOrigin, var chunk) in chunks)
{
var list = new List<NavMapLine>();
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = chunk.TileData & value;
if (mask == 0x0)
continue;
// Alright now we'll work out our edges
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
var position = new Vector2(tile.X, -tile.Y);
NavMapChunk? neighborChunk;
bool neighbor;
// North edge
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize)));
}
// East edge
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
}
// South edge
if (relativeTile.Y == 0)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position));
}
// West edge
if (relativeTile.X == 0)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
// Add point
list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize)));
}
// Draw a diagonal line for interiors.
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
}
decodedOutput.Add(chunkOrigin, list);
}
return decodedOutput;
}
protected Vector2 Scale(Vector2 position)
{ {
return position * MinimapScale + MidpointVector; return position * MinimapScale + MidpointVector;
} }
protected Vector2 GetOffset()
{
return _offset + (_physics != null ? _physics.LocalCenter : new Vector2());
}
}
public struct NavMapBlip
{
public EntityCoordinates Coordinates;
public Texture Texture;
public Color Color;
public bool Blinks;
public bool Selectable;
public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true)
{
Coordinates = coordinates;
Texture = texture;
Color = color;
Blinks = blinks;
Selectable = selectable;
}
}
public struct NavMapLine
{
public readonly Vector2 Origin;
public readonly Vector2 Terminus;
public NavMapLine(Vector2 origin, Vector2 terminus)
{
Origin = origin;
Terminus = terminus;
}
} }

View File

@@ -21,5 +21,7 @@ public sealed partial class StationMapWindow : FancyWindow
{ {
Title = metadata.EntityName; Title = metadata.EntityName;
} }
NavMapScreen.ForceNavMapUpdate();
} }
} }

View File

@@ -1,15 +1,14 @@
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Access.Components namespace Content.Server.Access.Components;
{
[RegisterComponent]
public sealed partial class PresetIdCardComponent : Component
{
[DataField("job")]
public ProtoId<JobPrototype>? JobName;
[DataField("name")] [RegisterComponent]
public string? IdName; public sealed partial class PresetIdCardComponent : Component
} {
[DataField("job")]
public ProtoId<JobPrototype>? JobName;
[DataField("name")]
public string? IdName;
} }

View File

@@ -7,200 +7,216 @@ using Content.Shared.Access.Components;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.StatusIcon; using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
namespace Content.Server.Access.Systems namespace Content.Server.Access.Systems;
public sealed class IdCardSystem : SharedIdCardSystem
{ {
public sealed class IdCardSystem : SharedIdCardSystem [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
public override void Initialize()
{ {
[Dependency] private readonly PopupSystem _popupSystem = default!; base.Initialize();
[Dependency] private readonly IRobustRandom _random = default!; SubscribeLocalEvent<IdCardComponent, MapInitEvent>(OnMapInit);
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; SubscribeLocalEvent<IdCardComponent, BeingMicrowavedEvent>(OnMicrowaved);
[Dependency] private readonly IAdminLogManager _adminLogger = default!; }
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
public override void Initialize() private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args)
{ {
base.Initialize(); UpdateEntityName(uid, id);
SubscribeLocalEvent<IdCardComponent, MapInitEvent>(OnMapInit); }
SubscribeLocalEvent<IdCardComponent, BeingMicrowavedEvent>(OnMicrowaved);
}
private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args) private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
{
if (TryComp<AccessComponent>(uid, out var access))
{ {
UpdateEntityName(uid, id); float randomPick = _random.NextFloat();
} // if really unlucky, burn card
if (randomPick <= 0.15f)
private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
{
if (TryComp<AccessComponent>(uid, out var access))
{ {
float randomPick = _random.NextFloat(); TryComp(uid, out TransformComponent? transformComponent);
// if really unlucky, burn card if (transformComponent != null)
if (randomPick <= 0.15f)
{ {
TryComp(uid, out TransformComponent? transformComponent); _popupSystem.PopupCoordinates(Loc.GetString("id-card-component-microwave-burnt", ("id", uid)),
if (transformComponent != null) transformComponent.Coordinates, PopupType.Medium);
{ EntityManager.SpawnEntity("FoodBadRecipe",
_popupSystem.PopupCoordinates(Loc.GetString("id-card-component-microwave-burnt", ("id", uid)), transformComponent.Coordinates);
transformComponent.Coordinates, PopupType.Medium);
EntityManager.SpawnEntity("FoodBadRecipe",
transformComponent.Coordinates);
}
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.Microwave)} burnt {ToPrettyString(uid):entity}");
EntityManager.QueueDeleteEntity(uid);
return;
} }
// If they're unlucky, brick their ID _adminLogger.Add(LogType.Action, LogImpact.Medium,
if (randomPick <= 0.25f) $"{ToPrettyString(args.Microwave)} burnt {ToPrettyString(uid):entity}");
{ EntityManager.QueueDeleteEntity(uid);
_popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-bricked", ("id", uid)), uid); return;
}
// If they're unlucky, brick their ID
if (randomPick <= 0.25f)
{
_popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-bricked", ("id", uid)), uid);
access.Tags.Clear(); access.Tags.Clear();
Dirty(access);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.Microwave)} cleared access on {ToPrettyString(uid):entity}");
}
else
{
_popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-safe", ("id", uid)), uid, PopupType.Medium);
}
// Give them a wonderful new access to compensate for everything
var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
access.Tags.Add(random.ID);
Dirty(access); Dirty(access);
_adminLogger.Add(LogType.Action, LogImpact.Medium, _adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.Microwave)} added {random.ID} access to {ToPrettyString(uid):entity}"); $"{ToPrettyString(args.Microwave)} cleared access on {ToPrettyString(uid):entity}");
}
}
/// <summary>
/// Attempts to change the job title of a card.
/// Returns true/false.
/// </summary>
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (!string.IsNullOrWhiteSpace(jobTitle))
{
jobTitle = jobTitle.Trim();
if (jobTitle.Length > IdCardConsoleComponent.MaxJobTitleLength)
jobTitle = jobTitle[..IdCardConsoleComponent.MaxJobTitleLength];
} }
else else
{ {
jobTitle = null; _popupSystem.PopupEntity(Loc.GetString("id-card-component-microwave-safe", ("id", uid)), uid, PopupType.Medium);
} }
if (id.JobTitle == jobTitle) // Give them a wonderful new access to compensate for everything
return true; var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
id.JobTitle = jobTitle;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null) access.Tags.Add(random.ID);
{ Dirty(access);
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(uid):entity} to {jobTitle} ");
}
return true;
}
public bool TryChangeJobIcon(EntityUid uid, StatusIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null) _adminLogger.Add(LogType.Action, LogImpact.Medium,
{ $"{ToPrettyString(args.Microwave)} added {random.ID} access to {ToPrettyString(uid):entity}");
if (!Resolve(uid, ref id))
{
return false;
}
if (id.JobIcon == jobIcon.ID)
{
return true;
}
id.JobIcon = jobIcon.ID;
Dirty(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the job icon of {ToPrettyString(uid):entity} to {jobIcon} ");
}
return true;
}
/// <summary>
/// Attempts to change the full name of a card.
/// Returns true/false.
/// </summary>
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (!string.IsNullOrWhiteSpace(fullName))
{
fullName = fullName.Trim();
if (fullName.Length > IdCardConsoleComponent.MaxFullNameLength)
fullName = fullName[..IdCardConsoleComponent.MaxFullNameLength];
}
else
{
fullName = null;
}
if (id.FullName == fullName)
return true;
id.FullName = fullName;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(uid):entity} to {fullName} ");
}
return true;
}
/// <summary>
/// Changes the name of the id's owner.
/// </summary>
/// <remarks>
/// If either <see cref="FullName"/> or <see cref="JobTitle"/> is empty, it's replaced by placeholders.
/// If both are empty, the original entity's name is restored.
/// </remarks>
private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null)
{
if (!Resolve(uid, ref id))
return;
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
("fullName", id.FullName),
("jobSuffix", jobSuffix));
_metaSystem.SetEntityName(uid, val);
} }
} }
/// <summary>
/// Attempts to change the job title of a card.
/// Returns true/false.
/// </summary>
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (!string.IsNullOrWhiteSpace(jobTitle))
{
jobTitle = jobTitle.Trim();
if (jobTitle.Length > IdCardConsoleComponent.MaxJobTitleLength)
jobTitle = jobTitle[..IdCardConsoleComponent.MaxJobTitleLength];
}
else
{
jobTitle = null;
}
if (id.JobTitle == jobTitle)
return true;
id.JobTitle = jobTitle;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(uid):entity} to {jobTitle} ");
}
return true;
}
public bool TryChangeJobIcon(EntityUid uid, StatusIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
{
return false;
}
if (id.JobIcon == jobIcon.ID)
{
return true;
}
id.JobIcon = jobIcon.ID;
Dirty(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the job icon of {ToPrettyString(uid):entity} to {jobIcon} ");
}
return true;
}
public bool TryChangeJobDepartment(EntityUid uid, JobPrototype job, IdCardComponent? id = null)
{
if (!Resolve(uid, ref id))
return false;
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
if (department.Roles.Contains(job.ID))
id.JobDepartments.Add("department-" + department.ID);
}
Dirty(uid, id);
return true;
}
/// <summary>
/// Attempts to change the full name of a card.
/// Returns true/false.
/// </summary>
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (!string.IsNullOrWhiteSpace(fullName))
{
fullName = fullName.Trim();
if (fullName.Length > IdCardConsoleComponent.MaxFullNameLength)
fullName = fullName[..IdCardConsoleComponent.MaxFullNameLength];
}
else
{
fullName = null;
}
if (id.FullName == fullName)
return true;
id.FullName = fullName;
Dirty(id);
UpdateEntityName(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(uid):entity} to {fullName} ");
}
return true;
}
/// <summary>
/// Changes the name of the id's owner.
/// </summary>
/// <remarks>
/// If either <see cref="FullName"/> or <see cref="JobTitle"/> is empty, it's replaced by placeholders.
/// If both are empty, the original entity's name is restored.
/// </remarks>
private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null)
{
if (!Resolve(uid, ref id))
return;
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
("fullName", id.FullName),
("jobSuffix", jobSuffix));
_metaSystem.SetEntityName(uid, val);
}
} }

View File

@@ -7,82 +7,82 @@ using Content.Shared.Roles;
using Content.Shared.StatusIcon; using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Access.Systems namespace Content.Server.Access.Systems;
public sealed class PresetIdCardSystem : EntitySystem
{ {
public sealed class PresetIdCardSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IdCardSystem _cardSystem = default!;
[Dependency] private readonly SharedAccessSystem _accessSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
public override void Initialize()
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; SubscribeLocalEvent<PresetIdCardComponent, MapInitEvent>(OnMapInit);
[Dependency] private readonly IdCardSystem _cardSystem = default!;
[Dependency] private readonly SharedAccessSystem _accessSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
public override void Initialize() SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(PlayerJobsAssigned);
}
private void PlayerJobsAssigned(RulePlayerJobsAssignedEvent ev)
{
// Go over all ID cards and make sure they're correctly configured for extended access.
var query = EntityQueryEnumerator<PresetIdCardComponent>();
while (query.MoveNext(out var uid, out var card))
{ {
SubscribeLocalEvent<PresetIdCardComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(PlayerJobsAssigned);
}
private void PlayerJobsAssigned(RulePlayerJobsAssignedEvent ev)
{
// Go over all ID cards and make sure they're correctly configured for extended access.
var query = EntityQueryEnumerator<PresetIdCardComponent>();
while (query.MoveNext(out var uid, out var card))
{
var station = _stationSystem.GetOwningStation(uid);
// If we're not on an extended access station, the ID is already configured correctly from MapInit.
if (station == null || !Comp<StationJobsComponent>(station.Value).ExtendedAccess)
return;
SetupIdAccess(uid, card, true);
SetupIdName(uid, card);
}
}
private void OnMapInit(EntityUid uid, PresetIdCardComponent id, MapInitEvent args)
{
// If a preset ID card is spawned on a station at setup time,
// the station may not exist,
// or may not yet know whether it is on extended access (players not spawned yet).
// PlayerJobsAssigned makes sure extended access is configured correctly in that case.
var station = _stationSystem.GetOwningStation(uid); var station = _stationSystem.GetOwningStation(uid);
var extended = false;
if (station != null)
extended = Comp<StationJobsComponent>(station.Value).ExtendedAccess;
SetupIdAccess(uid, id, extended); // If we're not on an extended access station, the ID is already configured correctly from MapInit.
SetupIdName(uid, id); if (station == null || !Comp<StationJobsComponent>(station.Value).ExtendedAccess)
return;
SetupIdAccess(uid, card, true);
SetupIdName(uid, card);
}
}
private void OnMapInit(EntityUid uid, PresetIdCardComponent id, MapInitEvent args)
{
// If a preset ID card is spawned on a station at setup time,
// the station may not exist,
// or may not yet know whether it is on extended access (players not spawned yet).
// PlayerJobsAssigned makes sure extended access is configured correctly in that case.
var station = _stationSystem.GetOwningStation(uid);
var extended = false;
if (station != null)
extended = Comp<StationJobsComponent>(station.Value).ExtendedAccess;
SetupIdAccess(uid, id, extended);
SetupIdName(uid, id);
}
private void SetupIdName(EntityUid uid, PresetIdCardComponent id)
{
if (id.IdName == null)
return;
_cardSystem.TryChangeFullName(uid, id.IdName);
}
private void SetupIdAccess(EntityUid uid, PresetIdCardComponent id, bool extended)
{
if (id.JobName == null)
return;
if (!_prototypeManager.TryIndex(id.JobName, out JobPrototype? job))
{
Log.Error($"Invalid job id ({id.JobName}) for preset card");
return;
} }
private void SetupIdName(EntityUid uid, PresetIdCardComponent id) _accessSystem.SetAccessToJob(uid, job, extended);
_cardSystem.TryChangeJobTitle(uid, job.LocalizedName);
_cardSystem.TryChangeJobDepartment(uid, job);
if (_prototypeManager.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
{ {
if (id.IdName == null) _cardSystem.TryChangeJobIcon(uid, jobIcon);
return;
_cardSystem.TryChangeFullName(uid, id.IdName);
}
private void SetupIdAccess(EntityUid uid, PresetIdCardComponent id, bool extended)
{
if (id.JobName == null)
return;
if (!_prototypeManager.TryIndex(id.JobName, out JobPrototype? job))
{
Log.Error($"Invalid job id ({id.JobName}) for preset card");
return;
}
_accessSystem.SetAccessToJob(uid, job, extended);
_cardSystem.TryChangeJobTitle(uid, job.LocalizedName);
if (_prototypeManager.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
{
_cardSystem.TryChangeJobIcon(uid, jobIcon);
}
} }
} }
} }

View File

@@ -1,33 +1,19 @@
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
namespace Content.Server.Medical.CrewMonitoring namespace Content.Server.Medical.CrewMonitoring;
[RegisterComponent]
[Access(typeof(CrewMonitoringConsoleSystem))]
public sealed partial class CrewMonitoringConsoleComponent : Component
{ {
[RegisterComponent] /// <summary>
[Access(typeof(CrewMonitoringConsoleSystem))] /// List of all currently connected sensors to this console.
public sealed partial class CrewMonitoringConsoleComponent : Component /// </summary>
{ public Dictionary<string, SuitSensorStatus> ConnectedSensors = new();
/// <summary>
/// List of all currently connected sensors to this console.
/// </summary>
public Dictionary<string, SuitSensorStatus> ConnectedSensors = new();
/// <summary> /// <summary>
/// After what time sensor consider to be lost. /// After what time sensor consider to be lost.
/// </summary> /// </summary>
[DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)] [DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)]
public float SensorTimeout = 10f; public float SensorTimeout = 10f;
/// <summary>
/// Whether the direction arrows in the monitor UI should snap the nearest diagonal or cardinal direction, or whether they should point exactly towards the target.
/// </summary>
[DataField("snap"), ViewVariables(VVAccess.ReadWrite)]
public bool Snap = true;
/// <summary>
/// Minimum distance before the monitor direction indicator stops pointing towards the target and instead
/// shows an icon indicating that the target is "here". Does not affect the displayed coordinates.
/// </summary>
[DataField("precision"), ViewVariables(VVAccess.ReadWrite)]
public float Precision = 10f;
}
} }

View File

@@ -4,62 +4,71 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.PowerCell; using Content.Server.PowerCell;
using Content.Shared.Medical.CrewMonitoring; using Content.Shared.Medical.CrewMonitoring;
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Content.Shared.Pinpointer;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
namespace Content.Server.Medical.CrewMonitoring namespace Content.Server.Medical.CrewMonitoring;
public sealed class CrewMonitoringConsoleSystem : EntitySystem
{ {
public sealed class CrewMonitoringConsoleSystem : EntitySystem [Dependency] private readonly PowerCellSystem _cell = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{ {
[Dependency] private readonly PowerCellSystem _cell = default!; base.Initialize();
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; SubscribeLocalEvent<CrewMonitoringConsoleComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<CrewMonitoringConsoleComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<CrewMonitoringConsoleComponent, BoundUIOpenedEvent>(OnUIOpened);
}
public override void Initialize() private void OnRemove(EntityUid uid, CrewMonitoringConsoleComponent component, ComponentRemove args)
{ {
base.Initialize(); component.ConnectedSensors.Clear();
SubscribeLocalEvent<CrewMonitoringConsoleComponent, ComponentRemove>(OnRemove); }
SubscribeLocalEvent<CrewMonitoringConsoleComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<CrewMonitoringConsoleComponent, BoundUIOpenedEvent>(OnUIOpened);
}
private void OnRemove(EntityUid uid, CrewMonitoringConsoleComponent component, ComponentRemove args) private void OnPacketReceived(EntityUid uid, CrewMonitoringConsoleComponent component, DeviceNetworkPacketEvent args)
{ {
component.ConnectedSensors.Clear(); var payload = args.Data;
}
private void OnPacketReceived(EntityUid uid, CrewMonitoringConsoleComponent component, DeviceNetworkPacketEvent args) // Check command
{ if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
var payload = args.Data; return;
// check command
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return;
if (command != DeviceNetworkConstants.CmdUpdatedState)
return;
if (!payload.TryGetValue(SuitSensorConstants.NET_STATUS_COLLECTION, out Dictionary<string, SuitSensorStatus>? sensorStatus))
return;
component.ConnectedSensors = sensorStatus; if (command != DeviceNetworkConstants.CmdUpdatedState)
UpdateUserInterface(uid, component); return;
}
private void OnUIOpened(EntityUid uid, CrewMonitoringConsoleComponent component, BoundUIOpenedEvent args) if (!payload.TryGetValue(SuitSensorConstants.NET_STATUS_COLLECTION, out Dictionary<string, SuitSensorStatus>? sensorStatus))
{ return;
if (!_cell.TryUseActivatableCharge(uid))
return;
UpdateUserInterface(uid, component); component.ConnectedSensors = sensorStatus;
} UpdateUserInterface(uid, component);
}
private void UpdateUserInterface(EntityUid uid, CrewMonitoringConsoleComponent? component = null) private void OnUIOpened(EntityUid uid, CrewMonitoringConsoleComponent component, BoundUIOpenedEvent args)
{ {
if (!Resolve(uid, ref component)) if (!_cell.TryUseActivatableCharge(uid))
return; return;
if (!_uiSystem.TryGetUi(uid, CrewMonitoringUIKey.Key, out var bui)) UpdateUserInterface(uid, component);
return; }
// update all sensors info private void UpdateUserInterface(EntityUid uid, CrewMonitoringConsoleComponent? component = null)
var allSensors = component.ConnectedSensors.Values.ToList(); {
_uiSystem.SetUiState(bui, new CrewMonitoringState(allSensors, component.Snap, component.Precision)); if (!Resolve(uid, ref component))
} return;
if (!_uiSystem.TryGetUi(uid, CrewMonitoringUIKey.Key, out var bui))
return;
// The grid must have a NavMapComponent to visualize the map in the UI
var xform = Transform(uid);
if (xform.GridUid != null)
EnsureComp<NavMapComponent>(xform.GridUid.Value);
// Update all sensors info
var allSensors = component.ConnectedSensors.Values.ToList();
_uiSystem.SetUiState(bui, new CrewMonitoringState(allSensors));
} }
} }

View File

@@ -1,76 +1,75 @@
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Medical.SuitSensors namespace Content.Server.Medical.SuitSensors;
/// <summary>
/// Tracking device, embedded in almost all uniforms and jumpsuits.
/// If enabled, will report to crew monitoring console owners position and status.
/// </summary>
[RegisterComponent]
[Access(typeof(SuitSensorSystem))]
public sealed partial class SuitSensorComponent : Component
{ {
/// <summary> /// <summary>
/// Tracking device, embedded in almost all uniforms and jumpsuits. /// Choose a random sensor mode when item is spawned.
/// If enabled, will report to crew monitoring console owners position and status.
/// </summary> /// </summary>
[RegisterComponent] [DataField("randomMode")]
[Access(typeof(SuitSensorSystem))] public bool RandomMode = true;
public sealed partial class SuitSensorComponent : Component
{
/// <summary>
/// Choose a random sensor mode when item is spawned.
/// </summary>
[DataField("randomMode")]
public bool RandomMode = true;
/// <summary> /// <summary>
/// If true user can't change suit sensor mode /// If true user can't change suit sensor mode
/// </summary> /// </summary>
[DataField("controlsLocked")] [DataField("controlsLocked")]
public bool ControlsLocked = false; public bool ControlsLocked = false;
/// <summary> /// <summary>
/// Current sensor mode. Can be switched by user verbs. /// Current sensor mode. Can be switched by user verbs.
/// </summary> /// </summary>
[DataField("mode")] [DataField("mode")]
public SuitSensorMode Mode = SuitSensorMode.SensorOff; public SuitSensorMode Mode = SuitSensorMode.SensorOff;
/// <summary> /// <summary>
/// Activate sensor if user wear it in this slot. /// Activate sensor if user wear it in this slot.
/// </summary> /// </summary>
[DataField("activationSlot")] [DataField("activationSlot")]
public string ActivationSlot = "jumpsuit"; public string ActivationSlot = "jumpsuit";
/// <summary> /// <summary>
/// Activate sensor if user has this in a sensor-compatible container. /// Activate sensor if user has this in a sensor-compatible container.
/// </summary> /// </summary>
[DataField("activationContainer")] [DataField("activationContainer")]
public string? ActivationContainer; public string? ActivationContainer;
/// <summary> /// <summary>
/// How often does sensor update its owners status (in seconds). Limited by the system update rate. /// How often does sensor update its owners status (in seconds). Limited by the system update rate.
/// </summary> /// </summary>
[DataField("updateRate")] [DataField("updateRate")]
public TimeSpan UpdateRate = TimeSpan.FromSeconds(2f); public TimeSpan UpdateRate = TimeSpan.FromSeconds(2f);
/// <summary> /// <summary>
/// Current user that wears suit sensor. Null if nobody wearing it. /// Current user that wears suit sensor. Null if nobody wearing it.
/// </summary> /// </summary>
[ViewVariables] [ViewVariables]
public EntityUid? User = null; public EntityUid? User = null;
/// <summary> /// <summary>
/// Next time when sensor updated owners status /// Next time when sensor updated owners status
/// </summary> /// </summary>
[DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))] [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero; public TimeSpan NextUpdate = TimeSpan.Zero;
/// <summary> /// <summary>
/// The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work. /// The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work.
/// </summary> /// </summary>
[DataField("station")] [DataField("station")]
public EntityUid? StationId = null; public EntityUid? StationId = null;
/// <summary> /// <summary>
/// The server the suit sensor sends it state to. /// The server the suit sensor sends it state to.
/// The suit sensor will try connecting to a new server when no server is connected. /// The suit sensor will try connecting to a new server when no server is connected.
/// It does this by calling the servers entity system for performance reasons. /// It does this by calling the servers entity system for performance reasons.
/// </summary> /// </summary>
[DataField("server")] [DataField("server")]
public string? ConnectedServer = null; public string? ConnectedServer = null;
}
} }

View File

@@ -18,399 +18,409 @@ using Robust.Shared.Map;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Medical.SuitSensors namespace Content.Server.Medical.SuitSensors;
public sealed class SuitSensorSystem : EntitySystem
{ {
public sealed class SuitSensorSystem : EntitySystem [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly IdCardSystem _idCardSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
public override void Initialize()
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; base.Initialize();
[Dependency] private readonly IRobustRandom _random = default!; SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
[Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!; SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; SubscribeLocalEvent<SuitSensorComponent, EntityUnpausedEvent>(OnUnpaused);
[Dependency] private readonly IdCardSystem _idCardSystem = default!; SubscribeLocalEvent<SuitSensorComponent, GotEquippedEvent>(OnEquipped);
[Dependency] private readonly MobStateSystem _mobStateSystem = default!; SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped);
[Dependency] private readonly PopupSystem _popupSystem = default!; SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
[Dependency] private readonly SharedTransformSystem _transform = default!; SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
[Dependency] private readonly StationSystem _stationSystem = default!; SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
}
public override void Initialize() private void OnUnpaused(EntityUid uid, SuitSensorComponent component, ref EntityUnpausedEvent args)
{
component.NextUpdate += args.PausedTime;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _gameTiming.CurTime;
var sensors = EntityManager.EntityQueryEnumerator<SuitSensorComponent, DeviceNetworkComponent>();
while (sensors.MoveNext(out var uid, out var sensor, out var device))
{ {
base.Initialize(); if (device.TransmitFrequency is null)
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn); continue;
SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SuitSensorComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<SuitSensorComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
}
private void OnUnpaused(EntityUid uid, SuitSensorComponent component, ref EntityUnpausedEvent args) // check if sensor is ready to update
{ if (curTime < sensor.NextUpdate)
component.NextUpdate += args.PausedTime; continue;
}
public override void Update(float frameTime) if (!CheckSensorAssignedStation(uid, sensor))
{ continue;
base.Update(frameTime);
var curTime = _gameTiming.CurTime; // TODO: This would cause imprecision at different tick rates.
var sensors = EntityManager.EntityQueryEnumerator<SuitSensorComponent, DeviceNetworkComponent>(); sensor.NextUpdate = curTime + sensor.UpdateRate;
while (sensors.MoveNext(out var uid, out var sensor, out var device)) // get sensor status
var status = GetSensorState(uid, sensor);
if (status == null)
continue;
//Retrieve active server address if the sensor isn't connected to a server
if (sensor.ConnectedServer == null)
{ {
if (device.TransmitFrequency is null) if (!_monitoringServerSystem.TryGetActiveServerAddress(sensor.StationId!.Value, out var address))
continue; continue;
// check if sensor is ready to update sensor.ConnectedServer = address;
if (curTime < sensor.NextUpdate)
continue;
if (!CheckSensorAssignedStation(uid, sensor))
continue;
// TODO: This would cause imprecision at different tick rates.
sensor.NextUpdate = curTime + sensor.UpdateRate;
// get sensor status
var status = GetSensorState(uid, sensor);
if (status == null)
continue;
//Retrieve active server address if the sensor isn't connected to a server
if (sensor.ConnectedServer == null)
{
if (!_monitoringServerSystem.TryGetActiveServerAddress(sensor.StationId!.Value, out var address))
continue;
sensor.ConnectedServer = address;
}
// Send it to the connected server
var payload = SuitSensorToPacket(status);
// Clear the connected server if its address isn't on the network
if (!_deviceNetworkSystem.IsAddressPresent(device.DeviceNetId, sensor.ConnectedServer))
{
sensor.ConnectedServer = null;
continue;
}
_deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device);
}
}
/// <summary>
/// Checks whether the sensor is assigned to a station or not
/// and tries to assign an unassigned sensor to a station if it's currently on a grid
/// </summary>
/// <returns>True if the sensor is assigned to a station or assigning it was successful. False otherwise.</returns>
private bool CheckSensorAssignedStation(EntityUid uid, SuitSensorComponent sensor)
{
if (!sensor.StationId.HasValue && Transform(uid).GridUid == null)
return false;
sensor.StationId = _stationSystem.GetOwningStation(uid);
return sensor.StationId.HasValue;
}
private void OnPlayerSpawn(PlayerSpawnCompleteEvent ev)
{
// If the player spawns in arrivals then the grid underneath them may not be appropriate.
// in which case we'll just use the station spawn code told us they are attached to and set all of their
// sensors.
var sensorQuery = GetEntityQuery<SuitSensorComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
RecursiveSensor(ev.Mob, ev.Station, sensorQuery, xformQuery);
}
private void RecursiveSensor(EntityUid uid, EntityUid stationUid, EntityQuery<SuitSensorComponent> sensorQuery, EntityQuery<TransformComponent> xformQuery)
{
var xform = xformQuery.GetComponent(uid);
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (sensorQuery.TryGetComponent(child, out var sensor))
{
sensor.StationId = stationUid;
}
RecursiveSensor(child.Value, stationUid, sensorQuery, xformQuery);
}
}
private void OnMapInit(EntityUid uid, SuitSensorComponent component, MapInitEvent args)
{
// Fallback
component.StationId ??= _stationSystem.GetOwningStation(uid);
// generate random mode
if (component.RandomMode)
{
//make the sensor mode favor higher levels, except coords.
var modesDist = new[]
{
SuitSensorMode.SensorOff,
SuitSensorMode.SensorBinary, SuitSensorMode.SensorBinary,
SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals,
SuitSensorMode.SensorCords, SuitSensorMode.SensorCords
};
component.Mode = _random.Pick(modesDist);
}
}
private void OnEquipped(EntityUid uid, SuitSensorComponent component, GotEquippedEvent args)
{
if (args.Slot != component.ActivationSlot)
return;
component.User = args.Equipee;
}
private void OnUnequipped(EntityUid uid, SuitSensorComponent component, GotUnequippedEvent args)
{
if (args.Slot != component.ActivationSlot)
return;
component.User = null;
}
private void OnExamine(EntityUid uid, SuitSensorComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
string msg;
switch (component.Mode)
{
case SuitSensorMode.SensorOff:
msg = "suit-sensor-examine-off";
break;
case SuitSensorMode.SensorBinary:
msg = "suit-sensor-examine-binary";
break;
case SuitSensorMode.SensorVitals:
msg = "suit-sensor-examine-vitals";
break;
case SuitSensorMode.SensorCords:
msg = "suit-sensor-examine-cords";
break;
default:
return;
} }
args.PushMarkup(Loc.GetString(msg)); // Send it to the connected server
} var payload = SuitSensorToPacket(status);
private void OnVerb(EntityUid uid, SuitSensorComponent component, GetVerbsEvent<Verb> args) // Clear the connected server if its address isn't on the network
{ if (!_deviceNetworkSystem.IsAddressPresent(device.DeviceNetId, sensor.ConnectedServer))
// check if user can change sensor
if (component.ControlsLocked)
return;
// standard interaction checks
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
return;
args.Verbs.UnionWith(new[]
{ {
CreateVerb(uid, component, args.User, SuitSensorMode.SensorOff), sensor.ConnectedServer = null;
CreateVerb(uid, component, args.User, SuitSensorMode.SensorBinary), continue;
CreateVerb(uid, component, args.User, SuitSensorMode.SensorVitals),
CreateVerb(uid, component, args.User, SuitSensorMode.SensorCords)
});
}
private void OnInsert(EntityUid uid, SuitSensorComponent component, EntGotInsertedIntoContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = args.Container.Owner;
}
private void OnRemove(EntityUid uid, SuitSensorComponent component, EntGotRemovedFromContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = null;
}
private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode)
{
return new Verb()
{
Text = GetModeName(mode),
Disabled = component.Mode == mode,
Priority = -(int) mode, // sort them in descending order
Category = VerbCategory.SetSensor,
Act = () => SetSensor(uid, mode, userUid, component)
};
}
private string GetModeName(SuitSensorMode mode)
{
string name;
switch (mode)
{
case SuitSensorMode.SensorOff:
name = "suit-sensor-mode-off";
break;
case SuitSensorMode.SensorBinary:
name = "suit-sensor-mode-binary";
break;
case SuitSensorMode.SensorVitals:
name = "suit-sensor-mode-vitals";
break;
case SuitSensorMode.SensorCords:
name = "suit-sensor-mode-cords";
break;
default:
return "";
} }
return Loc.GetString(name); _deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device);
}
public void SetSensor(EntityUid uid, SuitSensorMode mode, EntityUid? userUid = null,
SuitSensorComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Mode = mode;
if (userUid != null)
{
var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode)));
_popupSystem.PopupEntity(msg, uid, userUid.Value);
}
}
public SuitSensorStatus? GetSensorState(EntityUid uid, SuitSensorComponent? sensor = null, TransformComponent? transform = null)
{
if (!Resolve(uid, ref sensor, ref transform))
return null;
// check if sensor is enabled and worn by user
if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || transform.GridUid == null)
return null;
// try to get mobs id from ID slot
var userName = Loc.GetString("suit-sensor-component-unknown-name");
var userJob = Loc.GetString("suit-sensor-component-unknown-job");
if (_idCardSystem.TryFindIdCard(sensor.User.Value, out var card))
{
if (card.Comp.FullName != null)
userName = card.Comp.FullName;
if (card.Comp.JobTitle != null)
userJob = card.Comp.JobTitle;
}
// get health mob state
var isAlive = false;
if (EntityManager.TryGetComponent(sensor.User.Value, out MobStateComponent? mobState))
isAlive = !_mobStateSystem.IsDead(sensor.User.Value, mobState);
// get mob total damage
var totalDamage = 0;
if (TryComp<DamageableComponent>(sensor.User.Value, out var damageable))
totalDamage = damageable.TotalDamage.Int();
// finally, form suit sensor status
var status = new SuitSensorStatus(GetNetEntity(uid), userName, userJob);
switch (sensor.Mode)
{
case SuitSensorMode.SensorBinary:
status.IsAlive = isAlive;
break;
case SuitSensorMode.SensorVitals:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
break;
case SuitSensorMode.SensorCords:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
EntityCoordinates coordinates;
var xformQuery = GetEntityQuery<TransformComponent>();
if (transform.GridUid != null)
{
coordinates = new EntityCoordinates(transform.GridUid.Value,
_transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery)
.Transform(_transform.GetWorldPosition(transform, xformQuery)));
}
else if (transform.MapUid != null)
{
coordinates = new EntityCoordinates(transform.MapUid.Value,
_transform.GetWorldPosition(transform, xformQuery));
}
else
{
coordinates = EntityCoordinates.Invalid;
}
status.Coordinates = GetNetCoordinates(coordinates);
break;
}
return status;
}
/// <summary>
/// Serialize create a device network package from the suit sensors status.
/// </summary>
public NetworkPayload SuitSensorToPacket(SuitSensorStatus status)
{
var payload = new NetworkPayload()
{
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
[SuitSensorConstants.NET_NAME] = status.Name,
[SuitSensorConstants.NET_JOB] = status.Job,
[SuitSensorConstants.NET_IS_ALIVE] = status.IsAlive,
[SuitSensorConstants.NET_SUIT_SENSOR_UID] = status.SuitSensorUid,
};
if (status.TotalDamage != null)
payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
if (status.Coordinates != null)
payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates);
return payload;
}
/// <summary>
/// Try to create the suit sensors status from the device network message
/// </summary>
public SuitSensorStatus? PacketToSuitSensor(NetworkPayload payload)
{
// check command
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return null;
if (command != DeviceNetworkConstants.CmdUpdatedState)
return null;
// check name, job and alive
if (!payload.TryGetValue(SuitSensorConstants.NET_NAME, out string? name)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB, out string? job)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_IS_ALIVE, out bool? isAlive)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_SUIT_SENSOR_UID, out NetEntity suitSensorUid)) return null;
// try get total damage and cords (optionals)
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords);
var status = new SuitSensorStatus(suitSensorUid, name, job)
{
IsAlive = isAlive.Value,
TotalDamage = totalDamage,
Coordinates = coords,
};
return status;
} }
} }
/// <summary>
/// Checks whether the sensor is assigned to a station or not
/// and tries to assign an unassigned sensor to a station if it's currently on a grid
/// </summary>
/// <returns>True if the sensor is assigned to a station or assigning it was successful. False otherwise.</returns>
private bool CheckSensorAssignedStation(EntityUid uid, SuitSensorComponent sensor)
{
if (!sensor.StationId.HasValue && Transform(uid).GridUid == null)
return false;
sensor.StationId = _stationSystem.GetOwningStation(uid);
return sensor.StationId.HasValue;
}
private void OnPlayerSpawn(PlayerSpawnCompleteEvent ev)
{
// If the player spawns in arrivals then the grid underneath them may not be appropriate.
// in which case we'll just use the station spawn code told us they are attached to and set all of their
// sensors.
var sensorQuery = GetEntityQuery<SuitSensorComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
RecursiveSensor(ev.Mob, ev.Station, sensorQuery, xformQuery);
}
private void RecursiveSensor(EntityUid uid, EntityUid stationUid, EntityQuery<SuitSensorComponent> sensorQuery, EntityQuery<TransformComponent> xformQuery)
{
var xform = xformQuery.GetComponent(uid);
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (sensorQuery.TryGetComponent(child, out var sensor))
{
sensor.StationId = stationUid;
}
RecursiveSensor(child.Value, stationUid, sensorQuery, xformQuery);
}
}
private void OnMapInit(EntityUid uid, SuitSensorComponent component, MapInitEvent args)
{
// Fallback
component.StationId ??= _stationSystem.GetOwningStation(uid);
// generate random mode
if (component.RandomMode)
{
//make the sensor mode favor higher levels, except coords.
var modesDist = new[]
{
SuitSensorMode.SensorOff,
SuitSensorMode.SensorBinary, SuitSensorMode.SensorBinary,
SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals,
SuitSensorMode.SensorCords, SuitSensorMode.SensorCords
};
component.Mode = _random.Pick(modesDist);
}
}
private void OnEquipped(EntityUid uid, SuitSensorComponent component, GotEquippedEvent args)
{
if (args.Slot != component.ActivationSlot)
return;
component.User = args.Equipee;
}
private void OnUnequipped(EntityUid uid, SuitSensorComponent component, GotUnequippedEvent args)
{
if (args.Slot != component.ActivationSlot)
return;
component.User = null;
}
private void OnExamine(EntityUid uid, SuitSensorComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
string msg;
switch (component.Mode)
{
case SuitSensorMode.SensorOff:
msg = "suit-sensor-examine-off";
break;
case SuitSensorMode.SensorBinary:
msg = "suit-sensor-examine-binary";
break;
case SuitSensorMode.SensorVitals:
msg = "suit-sensor-examine-vitals";
break;
case SuitSensorMode.SensorCords:
msg = "suit-sensor-examine-cords";
break;
default:
return;
}
args.PushMarkup(Loc.GetString(msg));
}
private void OnVerb(EntityUid uid, SuitSensorComponent component, GetVerbsEvent<Verb> args)
{
// check if user can change sensor
if (component.ControlsLocked)
return;
// standard interaction checks
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
return;
args.Verbs.UnionWith(new[]
{
CreateVerb(uid, component, args.User, SuitSensorMode.SensorOff),
CreateVerb(uid, component, args.User, SuitSensorMode.SensorBinary),
CreateVerb(uid, component, args.User, SuitSensorMode.SensorVitals),
CreateVerb(uid, component, args.User, SuitSensorMode.SensorCords)
});
}
private void OnInsert(EntityUid uid, SuitSensorComponent component, EntGotInsertedIntoContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = args.Container.Owner;
}
private void OnRemove(EntityUid uid, SuitSensorComponent component, EntGotRemovedFromContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = null;
}
private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode)
{
return new Verb()
{
Text = GetModeName(mode),
Disabled = component.Mode == mode,
Priority = -(int) mode, // sort them in descending order
Category = VerbCategory.SetSensor,
Act = () => SetSensor(uid, mode, userUid, component)
};
}
private string GetModeName(SuitSensorMode mode)
{
string name;
switch (mode)
{
case SuitSensorMode.SensorOff:
name = "suit-sensor-mode-off";
break;
case SuitSensorMode.SensorBinary:
name = "suit-sensor-mode-binary";
break;
case SuitSensorMode.SensorVitals:
name = "suit-sensor-mode-vitals";
break;
case SuitSensorMode.SensorCords:
name = "suit-sensor-mode-cords";
break;
default:
return "";
}
return Loc.GetString(name);
}
public void SetSensor(EntityUid uid, SuitSensorMode mode, EntityUid? userUid = null,
SuitSensorComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Mode = mode;
if (userUid != null)
{
var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode)));
_popupSystem.PopupEntity(msg, uid, userUid.Value);
}
}
public SuitSensorStatus? GetSensorState(EntityUid uid, SuitSensorComponent? sensor = null, TransformComponent? transform = null)
{
if (!Resolve(uid, ref sensor, ref transform))
return null;
// check if sensor is enabled and worn by user
if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || transform.GridUid == null)
return null;
// try to get mobs id from ID slot
var userName = Loc.GetString("suit-sensor-component-unknown-name");
var userJob = Loc.GetString("suit-sensor-component-unknown-job");
var userJobIcon = "JobIconNoId";
var userJobDepartments = new List<string>();
if (_idCardSystem.TryFindIdCard(sensor.User.Value, out var card))
{
if (card.Comp.FullName != null)
userName = card.Comp.FullName;
if (card.Comp.JobTitle != null)
userJob = card.Comp.JobTitle;
if (card.Comp.JobIcon != null)
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)
userJobDepartments.Add(Loc.GetString(department));
}
// get health mob state
var isAlive = false;
if (EntityManager.TryGetComponent(sensor.User.Value, out MobStateComponent? mobState))
isAlive = !_mobStateSystem.IsDead(sensor.User.Value, mobState);
// get mob total damage
var totalDamage = 0;
if (TryComp<DamageableComponent>(sensor.User.Value, out var damageable))
totalDamage = damageable.TotalDamage.Int();
// finally, form suit sensor status
var status = new SuitSensorStatus(GetNetEntity(uid), userName, userJob, userJobIcon, userJobDepartments);
switch (sensor.Mode)
{
case SuitSensorMode.SensorBinary:
status.IsAlive = isAlive;
break;
case SuitSensorMode.SensorVitals:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
break;
case SuitSensorMode.SensorCords:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
EntityCoordinates coordinates;
var xformQuery = GetEntityQuery<TransformComponent>();
if (transform.GridUid != null)
{
coordinates = new EntityCoordinates(transform.GridUid.Value,
_transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery)
.Transform(_transform.GetWorldPosition(transform, xformQuery)));
}
else if (transform.MapUid != null)
{
coordinates = new EntityCoordinates(transform.MapUid.Value,
_transform.GetWorldPosition(transform, xformQuery));
}
else
{
coordinates = EntityCoordinates.Invalid;
}
status.Coordinates = GetNetCoordinates(coordinates);
break;
}
return status;
}
/// <summary>
/// Serialize create a device network package from the suit sensors status.
/// </summary>
public NetworkPayload SuitSensorToPacket(SuitSensorStatus status)
{
var payload = new NetworkPayload()
{
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
[SuitSensorConstants.NET_NAME] = status.Name,
[SuitSensorConstants.NET_JOB] = status.Job,
[SuitSensorConstants.NET_JOB_ICON] = status.JobIcon,
[SuitSensorConstants.NET_JOB_DEPARTMENTS] = status.JobDepartments,
[SuitSensorConstants.NET_IS_ALIVE] = status.IsAlive,
[SuitSensorConstants.NET_SUIT_SENSOR_UID] = status.SuitSensorUid,
};
if (status.TotalDamage != null)
payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
if (status.Coordinates != null)
payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates);
return payload;
}
/// <summary>
/// Try to create the suit sensors status from the device network message
/// </summary>
public SuitSensorStatus? PacketToSuitSensor(NetworkPayload payload)
{
// check command
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return null;
if (command != DeviceNetworkConstants.CmdUpdatedState)
return null;
// check name, job and alive
if (!payload.TryGetValue(SuitSensorConstants.NET_NAME, out string? name)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB, out string? job)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_ICON, out string? jobIcon)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_DEPARTMENTS, out List<string>? jobDepartments)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_IS_ALIVE, out bool? isAlive)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_SUIT_SENSOR_UID, out NetEntity suitSensorUid)) return null;
// try get total damage and cords (optionals)
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords);
var status = new SuitSensorStatus(suitSensorUid, name, job, jobIcon, jobDepartments)
{
IsAlive = isAlive.Value,
TotalDamage = totalDamage,
Coordinates = coords,
};
return status;
}
} }

View File

@@ -4,29 +4,34 @@ using Content.Shared.StatusIcon;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Access.Components namespace Content.Shared.Access.Components;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)]
public sealed partial class IdCardComponent : Component
{ {
[RegisterComponent, NetworkedComponent] [DataField("fullName"), ViewVariables(VVAccess.ReadWrite)]
[AutoGenerateComponentState] [AutoNetworkedField]
[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)] // FIXME Friends
public sealed partial class IdCardComponent : Component public string? FullName;
{
[DataField("fullName"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
// FIXME Friends
public string? FullName;
[DataField("jobTitle")] [DataField("jobTitle")]
[AutoNetworkedField] [AutoNetworkedField]
[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite), ViewVariables(VVAccess.ReadWrite)] [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite), ViewVariables(VVAccess.ReadWrite)]
public string? JobTitle; public string? JobTitle;
/// <summary> /// <summary>
/// The state of the job icon rsi. /// The state of the job icon rsi.
/// </summary> /// </summary>
[DataField("jobIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))] [DataField("jobIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
[AutoNetworkedField] [AutoNetworkedField]
public string JobIcon = "JobIconUnknown"; public string JobIcon = "JobIconUnknown";
} /// <summary>
/// The unlocalized names of the departments associated with the job
/// </summary>
[DataField("jobDepartments")]
[AutoNetworkedField]
public List<LocId> JobDepartments = new();
} }

View File

@@ -1,27 +1,21 @@
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Medical.CrewMonitoring namespace Content.Shared.Medical.CrewMonitoring;
[Serializable, NetSerializable]
public enum CrewMonitoringUIKey
{ {
[Serializable, NetSerializable] Key
public enum CrewMonitoringUIKey }
{
Key [Serializable, NetSerializable]
} public sealed class CrewMonitoringState : BoundUserInterfaceState
{
[Serializable, NetSerializable] public List<SuitSensorStatus> Sensors;
public sealed class CrewMonitoringState : BoundUserInterfaceState
{ public CrewMonitoringState(List<SuitSensorStatus> sensors)
public List<SuitSensorStatus> Sensors; {
public readonly bool Snap; Sensors = sensors;
public readonly float Precision; }
public CrewMonitoringState(List<SuitSensorStatus> sensors, bool snap, float precision)
{
Sensors = sensors;
Snap = snap;
Precision = precision;
}
}
} }

View File

@@ -1,61 +1,66 @@
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Medical.SuitSensor namespace Content.Shared.Medical.SuitSensor;
[Serializable, NetSerializable]
public sealed class SuitSensorStatus
{ {
[Serializable, NetSerializable] public SuitSensorStatus(NetEntity suitSensorUid, string name, string job, string jobIcon, List<string> jobDepartments)
public sealed class SuitSensorStatus
{ {
public SuitSensorStatus(NetEntity suitSensorUid, string name, string job) SuitSensorUid = suitSensorUid;
{ Name = name;
SuitSensorUid = suitSensorUid; Job = job;
Name = name; JobIcon = jobIcon;
Job = job; JobDepartments = jobDepartments;
}
public TimeSpan Timestamp;
public NetEntity SuitSensorUid;
public string Name;
public string Job;
public bool IsAlive;
public int? TotalDamage;
public NetCoordinates? Coordinates;
} }
[Serializable, NetSerializable] public TimeSpan Timestamp;
public enum SuitSensorMode : byte public NetEntity SuitSensorUid;
{ public string Name;
/// <summary> public string Job;
/// Sensor doesn't send any information about owner public string JobIcon;
/// </summary> public List<string> JobDepartments;
SensorOff = 0, public bool IsAlive;
public int? TotalDamage;
/// <summary> public NetCoordinates? Coordinates;
/// Sensor sends only binary status (alive/dead) }
/// </summary>
SensorBinary = 1, [Serializable, NetSerializable]
public enum SuitSensorMode : byte
/// <summary> {
/// Sensor sends health vitals status /// <summary>
/// </summary> /// Sensor doesn't send any information about owner
SensorVitals = 2, /// </summary>
SensorOff = 0,
/// <summary>
/// Sensor sends vitals status and GPS position /// <summary>
/// </summary> /// Sensor sends only binary status (alive/dead)
SensorCords = 3 /// </summary>
} SensorBinary = 1,
public static class SuitSensorConstants /// <summary>
{ /// Sensor sends health vitals status
public const string NET_NAME = "name"; /// </summary>
public const string NET_JOB = "job"; SensorVitals = 2,
public const string NET_IS_ALIVE = "alive";
public const string NET_TOTAL_DAMAGE = "vitals"; /// <summary>
public const string NET_COORDINATES = "coords"; /// Sensor sends vitals status and GPS position
public const string NET_SUIT_SENSOR_UID = "uid"; /// </summary>
SensorCords = 3
///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor }
public const string NET_STATUS_COLLECTION = "suit-status-collection";
} public static class SuitSensorConstants
{
public const string NET_NAME = "name";
public const string NET_JOB = "job";
public const string NET_JOB_ICON = "jobIcon";
public const string NET_JOB_DEPARTMENTS = "jobDepartments";
public const string NET_IS_ALIVE = "alive";
public const string NET_TOTAL_DAMAGE = "vitals";
public const string NET_COORDINATES = "coords";
public const string NET_SUIT_SENSOR_UID = "uid";
///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor
public const string NET_STATUS_COLLECTION = "suit-status-collection";
} }

View File

@@ -1,6 +1,6 @@
## UI ## UI
crew-monitoring-user-interface-title = Crew Monitoring crew-monitoring-user-interface-title = Crew Monitoring Console
crew-monitoring-user-interface-name = Name crew-monitoring-user-interface-name = Name
crew-monitoring-user-interface-job = Job crew-monitoring-user-interface-job = Job
@@ -12,3 +12,8 @@ crew-monitoring-user-interface-dead = Dead
crew-monitoring-user-interface-no-info = N/A crew-monitoring-user-interface-no-info = N/A
crew-monitoring-user-interface-no-server = Server not found crew-monitoring-user-interface-no-server = Server not found
crew-monitoring-user-interface-no-department = Unknown
crew-monitoring-user-interface-flavor-left = In case of an emergancy, contact station medical staff immediately
crew-monitoring-user-interface-flavor-right = v1.7

View File

@@ -0,0 +1,3 @@
navmap-zoom = Zoom: {$value}%
navmap-recenter = Recenter
navmap-toggle-beacons = Show departments

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,41 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Created by chromiumboy, derived from https://github.com/tgstation/tgstation/commits/50689f89a40e5e7a2732a0c5fb38c787b69f7d28/icons/hud/screen_gen.dmi, ",
"size": {
"x": 24,
"y": 8
},
"states": [
{
"name": "alive"
},
{
"name": "dead"
},
{
"name": "health0"
},
{
"name": "health1"
},
{
"name": "health2"
},
{
"name": "health3"
},
{
"name": "health4"
},
{
"name": "critical",
"delays": [
[
0.35,
0.35
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB