diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 452f5be09f..c9c3d8df14 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -308,6 +308,8 @@ namespace Content.Client.Entry
"ExtensionCableReceiver",
"ExtensionCableProvider",
"ApcNetworkConnection",
+ "SuitSensor",
+ "CrewMonitoringConsole",
"ApcNetSwitch",
"HandLabeler",
"Label",
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs
new file mode 100644
index 0000000000..030ce917c6
--- /dev/null
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringBoundUserInterface.cs
@@ -0,0 +1,44 @@
+using Content.Shared.Medical.CrewMonitoring;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Shared.GameObjects;
+
+namespace Content.Client.Medical.CrewMonitoring
+{
+ public class CrewMonitoringBoundUserInterface : BoundUserInterface
+ {
+ private CrewMonitoringWindow? _menu;
+
+ public CrewMonitoringBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ _menu = new CrewMonitoringWindow();
+ _menu.OpenCentered();
+ _menu.OnClose += Close;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ switch (state)
+ {
+ case CrewMonitoringState st:
+ _menu?.ShowSensors(st.Sensors);
+ break;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+ }
+}
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
new file mode 100644
index 0000000000..d0d153f9d3
--- /dev/null
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
new file mode 100644
index 0000000000..1d5c13caf6
--- /dev/null
+++ b/Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using Content.Shared.Medical.SuitSensor;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Localization;
+
+namespace Content.Client.Medical.CrewMonitoring
+{
+ [GenerateTypedNameReferences]
+ public partial class CrewMonitoringWindow : SS14Window
+ {
+ private List _rowsContent = new();
+
+ public CrewMonitoringWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void ShowSensors(List stSensors)
+ {
+ ClearAllSensors();
+
+ // add a row for each sensor
+ foreach (var sensor in stSensors)
+ {
+ // add users name and job
+ // format: UserName (Job)
+ var nameLabel = new Label()
+ {
+ Text = $"{sensor.Name} ({sensor.Job})"
+ };
+ SensorsTable.AddChild(nameLabel);
+ _rowsContent.Add(nameLabel);
+
+ // 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
+ };
+ SensorsTable.AddChild(statusLabel);
+ _rowsContent.Add(statusLabel);
+
+ // add users positions
+ // format: (x, y)
+ string posText;
+ if (sensor.Coordinates != null)
+ {
+ // todo: add locations names (kitchen, bridge, etc)
+ var pos = sensor.Coordinates.Value.Position;
+ var x = (int) pos.X;
+ var y = (int) pos.Y;
+ posText = $"({x}, {y})";
+ }
+ else
+ {
+ posText = Loc.GetString("crew-monitoring-user-interface-no-info");
+ }
+ var posLabel = new Label()
+ {
+ Text = posText
+ };
+ SensorsTable.AddChild(posLabel);
+ _rowsContent.Add(posLabel);
+ }
+ }
+
+ private void ClearAllSensors()
+ {
+ foreach (var child in _rowsContent)
+ {
+ SensorsTable.RemoveChild(child);
+ }
+ _rowsContent.Clear();
+ }
+ }
+}
diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs
index c1bcaff58e..01ebef2251 100644
--- a/Content.Server/Access/Systems/IdCardSystem.cs
+++ b/Content.Server/Access/Systems/IdCardSystem.cs
@@ -101,22 +101,14 @@ namespace Content.Server.Access.Systems
return true;
// check inventory slot?
- if (EntityManager.TryGetComponent(uid, out InventoryComponent? inventoryComponent) &&
- inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) &&
- inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent? item) &&
- TryGetIdCard(item.Owner, out idCard))
- {
- return true;
- }
-
- return false;
+ return TryGetIdCardSlot(uid, out idCard);
}
///
/// Attempt to get an id card component from an entity, either by getting it directly from the entity, or by
/// getting the contained id from a .
///
- private bool TryGetIdCard(EntityUid uid, [NotNullWhen(true)] out IdCardComponent? idCard)
+ public bool TryGetIdCard(EntityUid uid, [NotNullWhen(true)] out IdCardComponent? idCard)
{
if (EntityManager.TryGetComponent(uid, out idCard))
return true;
@@ -129,5 +121,17 @@ namespace Content.Server.Access.Systems
return false;
}
+
+ ///
+ /// Try get id card from mobs ID inventory slot
+ ///
+ public bool TryGetIdCardSlot(EntityUid uid, [NotNullWhen(true)] out IdCardComponent? idCard)
+ {
+ idCard = null;
+ return EntityManager.TryGetComponent(uid, out InventoryComponent? inventoryComponent) &&
+ inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) &&
+ inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent? item) &&
+ TryGetIdCard(item.Owner, out idCard);
+ }
}
}
diff --git a/Content.Server/DeviceNetwork/DeviceNetworkConstants.cs b/Content.Server/DeviceNetwork/DeviceNetworkConstants.cs
index 6136a210ae..b8d53ee423 100644
--- a/Content.Server/DeviceNetwork/DeviceNetworkConstants.cs
+++ b/Content.Server/DeviceNetwork/DeviceNetworkConstants.cs
@@ -24,6 +24,12 @@ namespace Content.Server.DeviceNetwork
///
public const string CmdSetState = "set_state";
+ ///
+ /// The command for a device that just updated its state
+ /// E.g. suit sensors broadcasting owners vitals state
+ ///
+ public const string CmdUpdatedState = "updated_state";
+
#endregion
#region SetState
diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs
new file mode 100644
index 0000000000..0b98d4e362
--- /dev/null
+++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleComponent.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using Content.Shared.Medical.SuitSensor;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Medical.CrewMonitoring
+{
+ [RegisterComponent]
+ [Friend(typeof(CrewMonitoringConsoleSystem))]
+ public class CrewMonitoringConsoleComponent : Component
+ {
+ public override string Name => "CrewMonitoringConsole";
+
+ ///
+ /// List of all currently connected sensors to this console.
+ ///
+ public Dictionary ConnectedSensors = new();
+
+ ///
+ /// After what time sensor consider to be lost.
+ ///
+ [DataField("sensorTimeout")]
+ public float SensorTimeout = 10f;
+ }
+}
diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs
new file mode 100644
index 0000000000..607b1939ec
--- /dev/null
+++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs
@@ -0,0 +1,89 @@
+using System.Linq;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Server.Medical.SuitSensors;
+using Content.Server.UserInterface;
+using Content.Shared.Medical.CrewMonitoring;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Medical.CrewMonitoring
+{
+ public class CrewMonitoringConsoleSystem : EntitySystem
+ {
+ [Dependency] private readonly SuitSensorSystem _sensors = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ private const float UpdateRate = 3f;
+ private float _updateDif;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnRemove);
+ SubscribeLocalEvent(OnPacketReceived);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ // check update rate
+ _updateDif += frameTime;
+ if (_updateDif < UpdateRate)
+ return;
+ _updateDif = 0f;
+
+ var consoles = EntityManager.EntityQuery();
+ foreach (var console in consoles)
+ {
+ UpdateTimeouts(console.Owner, console);
+ UpdateUserInterface(console.Owner, console);
+ }
+ }
+
+ private void OnRemove(EntityUid uid, CrewMonitoringConsoleComponent component, ComponentRemove args)
+ {
+ component.ConnectedSensors.Clear();
+ }
+
+ private void OnPacketReceived(EntityUid uid, CrewMonitoringConsoleComponent component, PacketSentEvent args)
+ {
+ var suitSensor = _sensors.PacketToSuitSensor(args.Data);
+ if (suitSensor == null)
+ return;
+
+ suitSensor.Timestamp = _gameTiming.CurTime;
+ component.ConnectedSensors[args.SenderAddress] = suitSensor;
+ }
+
+ private void UpdateUserInterface(EntityUid uid, CrewMonitoringConsoleComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ var ui = component.Owner.GetUIOrNull(CrewMonitoringUIKey.Key);
+ if (ui == null)
+ return;
+
+ // update all sensors info
+ var allSensors = component.ConnectedSensors.Values.ToList();
+ var uiState = new CrewMonitoringState(allSensors);
+ ui.SetState(uiState);
+ }
+
+ private void UpdateTimeouts(EntityUid uid, CrewMonitoringConsoleComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ foreach (var (address, sensor) in component.ConnectedSensors)
+ {
+ // if too many time passed - sensor just dropped connection
+ var dif = _gameTiming.CurTime - sensor.Timestamp;
+ if (dif.Seconds > component.SensorTimeout)
+ component.ConnectedSensors.Remove(address);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs
new file mode 100644
index 0000000000..f5273fa553
--- /dev/null
+++ b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs
@@ -0,0 +1,59 @@
+using System;
+using Content.Shared.Inventory;
+using Content.Shared.Medical.SuitSensor;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Medical.SuitSensors
+{
+ ///
+ /// Tracking device, embedded in almost all uniforms and jumpsuits.
+ /// If enabled, will report to crew monitoring console owners position and status.
+ ///
+ [RegisterComponent]
+ [Friend(typeof(SuitSensorSystem))]
+ [ComponentProtoName("SuitSensor")]
+ public sealed class SuitSensorComponent : Component
+ {
+ ///
+ /// Choose a random sensor mode when item is spawned.
+ ///
+ [DataField("randomMode")]
+ public bool RandomMode = true;
+
+ ///
+ /// If true user can't change suit sensor mode
+ ///
+ [DataField("controlsLocked")]
+ public bool ControlsLocked = false;
+
+ ///
+ /// Current sensor mode. Can be switched by user verbs.
+ ///
+ [DataField("mode")]
+ public SuitSensorMode Mode = SuitSensorMode.SensorOff;
+
+ ///
+ /// Activate sensor if user wear it in this slot.
+ ///
+ [DataField("activationSlot")]
+ public EquipmentSlotDefines.Slots ActivationSlot = EquipmentSlotDefines.Slots.INNERCLOTHING;
+
+ ///
+ /// How often does sensor update its owners status (in seconds).
+ ///
+ [DataField("updateRate")]
+ public float UpdateRate = 2f;
+
+ ///
+ /// Current user that wears suit sensor. Null if nobody wearing it.
+ ///
+ public EntityUid? User = null;
+
+ ///
+ /// Last time when sensor updated owners status
+ ///
+ public TimeSpan LastUpdate = TimeSpan.Zero;
+ }
+}
diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
new file mode 100644
index 0000000000..e48fded7f4
--- /dev/null
+++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
@@ -0,0 +1,312 @@
+using System;
+using Content.Server.Access.Systems;
+using Content.Server.DeviceNetwork;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Server.Popups;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Damage;
+using Content.Shared.Examine;
+using Content.Shared.Inventory;
+using Content.Shared.Medical.SuitSensor;
+using Content.Shared.MobState.Components;
+using Content.Shared.Verbs;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Medical.SuitSensors
+{
+ public class SuitSensorSystem : EntitySystem
+ {
+ [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IdCardSystem _idCardSystem = default!;
+ [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ private const float UpdateRate = 0.5f;
+ private float _updateDif;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnEquipped);
+ SubscribeLocalEvent(OnUnequipped);
+ SubscribeLocalEvent(OnExamine);
+ SubscribeLocalEvent(OnVerb);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ // check update rate
+ _updateDif += frameTime;
+ if (_updateDif < UpdateRate)
+ return;
+
+ _updateDif -= UpdateRate;
+
+ var curTime = _gameTiming.CurTime;
+ var sensors = EntityManager.EntityQuery();
+ foreach (var (sensor, device) in sensors)
+ {
+ // check if sensor is ready to update
+ if (curTime - sensor.LastUpdate < TimeSpan.FromSeconds(sensor.UpdateRate))
+ continue;
+ sensor.LastUpdate = curTime;
+
+ // get sensor status
+ var status = GetSensorState(sensor.Owner, sensor);
+ if (status == null)
+ continue;
+
+ // broadcast it to device network
+ var payload = SuitSensorToPacket(status);
+ _deviceNetworkSystem.QueuePacket(sensor.Owner, DeviceNetworkConstants.NullAddress, device.Frequency, payload, true);
+ }
+ }
+
+ private void OnMapInit(EntityUid uid, SuitSensorComponent component, MapInitEvent args)
+ {
+ // 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, EquippedEvent args)
+ {
+ if (args.Slot != component.ActivationSlot)
+ return;
+
+ component.User = args.User;
+ }
+
+ private void OnUnequipped(EntityUid uid, SuitSensorComponent component, UnequippedEvent 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, GetInteractionVerbsEvent args)
+ {
+ // check if user can change sensor
+ if (component.ControlsLocked)
+ return;
+
+ // standard interaction checks
+ if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanDrop(args.User))
+ 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 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, Filter.Entities(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)
+ 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.TryGetIdCardSlot(sensor.User.Value, out var card))
+ {
+ if (card.FullName != null)
+ userName = card.FullName;
+ if (card.JobTitle != null)
+ userJob = card.JobTitle;
+ }
+
+ // get health mob state
+ var isAlive = false;
+ if (EntityManager.TryGetComponent(sensor.User.Value, out MobStateComponent? mobState))
+ {
+ isAlive = mobState.IsAlive();
+ }
+
+ // get mob total damage
+ var totalDamage = 0;
+ if (EntityManager.TryGetComponent(sensor.User.Value, out DamageableComponent? damageable))
+ {
+ totalDamage = damageable.TotalDamage.Int();
+ }
+
+ // finally, form suit sensor status
+ var status = new SuitSensorStatus(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;
+ status.Coordinates = transform.MapPosition;
+ break;
+ }
+
+ return status;
+ }
+
+ ///
+ /// Serialize suit sensor status into device network package.
+ ///
+ 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,
+ };
+
+ if (status.TotalDamage != null)
+ payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
+ if (status.Coordinates != null)
+ payload.Add(SuitSensorConstants.NET_CORDINATES, status.Coordinates);
+
+ return payload;
+ }
+
+ ///
+ /// Try to deserialize device network message into suit sensor status
+ ///
+ 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;
+
+ // try get total damage and cords (optionals)
+ payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
+ payload.TryGetValue(SuitSensorConstants.NET_CORDINATES, out MapCoordinates? cords);
+
+ var status = new SuitSensorStatus(name, job)
+ {
+ IsAlive = isAlive.Value,
+ TotalDamage = totalDamage,
+ Coordinates = cords
+ };
+ return status;
+ }
+ }
+}
diff --git a/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs b/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs
new file mode 100644
index 0000000000..53f19f1295
--- /dev/null
+++ b/Content.Shared/Medical/CrewMonitoring/CrewMonitoringShared.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using Content.Shared.FixedPoint;
+using Content.Shared.Medical.SuitSensor;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Medical.CrewMonitoring
+{
+ [Serializable, NetSerializable]
+ public enum CrewMonitoringUIKey
+ {
+ Key
+ }
+
+ [Serializable, NetSerializable]
+ public class CrewMonitoringState : BoundUserInterfaceState
+ {
+ public List Sensors;
+
+ public CrewMonitoringState(List sensors)
+ {
+ Sensors = sensors;
+ }
+ }
+
+}
diff --git a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs
new file mode 100644
index 0000000000..e717c6dad5
--- /dev/null
+++ b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs
@@ -0,0 +1,58 @@
+using System;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Medical.SuitSensor
+{
+ [Serializable, NetSerializable]
+ public class SuitSensorStatus
+ {
+ public SuitSensorStatus(string name, string job)
+ {
+ Name = name;
+ Job = job;
+ }
+
+ public TimeSpan Timestamp;
+ public string Name;
+ public string Job;
+ public bool IsAlive;
+ public int? TotalDamage;
+ public MapCoordinates? Coordinates;
+ }
+
+ [Serializable, NetSerializable]
+ public enum SuitSensorMode : byte
+ {
+ ///
+ /// Sensor doesn't send any information about owner
+ ///
+ SensorOff = 0,
+
+ ///
+ /// Sensor sends only binary status (alive/dead)
+ ///
+ SensorBinary = 1,
+
+ ///
+ /// Sensor sends health vitals status
+ ///
+ SensorVitals = 2,
+
+ ///
+ /// Sensor sends vitals status and GPS position
+ ///
+ SensorCords = 3
+ }
+
+ public static class SuitSensorConstants
+ {
+ public const string NET_NAME = "name";
+ public const string NET_JOB = "job";
+ public const string NET_IS_ALIVE = "alive";
+ public const string NET_TOTAL_DAMAGE = "vitals";
+ public const string NET_CORDINATES = "cords";
+ }
+}
diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs
index 45ec725028..dbc30eced3 100644
--- a/Content.Shared/Verbs/VerbCategory.cs
+++ b/Content.Shared/Verbs/VerbCategory.cs
@@ -57,5 +57,7 @@ namespace Content.Shared.Verbs
public static readonly VerbCategory Split =
new("verb-categories-split", null);
+
+ public static readonly VerbCategory SetSensor = new("verb-categories-set-sensor", null);
}
}
diff --git a/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl b/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl
new file mode 100644
index 0000000000..d2f4a95369
--- /dev/null
+++ b/Resources/Locale/en-US/medical/components/crew-monitoring-component.ftl
@@ -0,0 +1,13 @@
+## UI
+
+crew-monitoring-user-interface-title = Crew Monitoring
+
+crew-monitoring-user-interface-name = Name
+crew-monitoring-user-interface-status = Status
+crew-monitoring-user-interface-location = Location
+
+crew-monitoring-user-interface-alive = Alive
+crew-monitoring-user-interface-dead = Dead
+crew-monitoring-user-interface-no-info = N/A
+
+
diff --git a/Resources/Locale/en-US/medical/components/suit-sensor-component.ftl b/Resources/Locale/en-US/medical/components/suit-sensor-component.ftl
new file mode 100644
index 0000000000..993e726055
--- /dev/null
+++ b/Resources/Locale/en-US/medical/components/suit-sensor-component.ftl
@@ -0,0 +1,21 @@
+## Modes
+
+suit-sensor-mode-off = Off
+suit-sensor-mode-binary = Binary
+suit-sensor-mode-vitals = Vitals
+suit-sensor-mode-cords = Coordinates
+
+## Popups
+suit-sensor-mode-state = Suit sensors: {$mode}
+
+## Components
+
+suit-sensor-component-unknown-name = Unknown
+suit-sensor-component-unknown-job = No job
+
+## Examine
+
+suit-sensor-examine-off = Its sensors appear to be [color=darkred]disabled[/color].
+suit-sensor-examine-binary = Its binary life sensors appear to be enabled.
+suit-sensor-examine-vitals = Its vital tracker appears to be enabled.
+suit-sensor-examine-cords = Its vital tracker and tracking beacon appear to be enabled.
diff --git a/Resources/Locale/en-US/verbs/verb-system.ftl b/Resources/Locale/en-US/verbs/verb-system.ftl
index 9508eac7b9..ec9ff7763d 100644
--- a/Resources/Locale/en-US/verbs/verb-system.ftl
+++ b/Resources/Locale/en-US/verbs/verb-system.ftl
@@ -18,6 +18,7 @@ verb-categories-unbuckle = Unbuckle
verb-categories-rotate = Rotate
verb-categories-transfer = Set Transfer Amount
verb-categories-split = Split
+verb-categories-set-sensor = Sensor
verb-common-toggle-light = Toggle light
verb-common-close = Close
diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml
index 85e590e36c..31968062d9 100644
--- a/Resources/Prototypes/Catalog/Research/technologies.yml
+++ b/Resources/Prototypes/Catalog/Research/technologies.yml
@@ -113,6 +113,7 @@
- MedicalScannerMachineCircuitboard
- ChemMasterMachineCircuitboard
- ChemDispenserMachineCircuitboard
+ - CrewMonitoringComputerCircuitboard
# Security Technology Tree
diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml
index 76f196fb3f..54482b7164 100644
--- a/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml
+++ b/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml
@@ -1,6 +1,17 @@
- type: entity
abstract: true
parent: Clothing
+ id: ClothingWithSuitSensor
+ components:
+ - type: SuitSensor
+ - type: DeviceNetworkComponent
+ deviceNetId: Wireless
+ - type: WirelessNetworkConnection
+ range: 500
+
+- type: entity
+ abstract: true
+ parent: ClothingWithSuitSensor
id: ClothingUniformBase
components:
- type: Sprite
@@ -17,7 +28,7 @@
- type: entity
abstract: true
- parent: Clothing
+ parent: ClothingWithSuitSensor
id: ClothingUniformSkirtBase
components:
- type: Sprite
diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml
index f692843020..de5dc36778 100644
--- a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml
+++ b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml
@@ -232,6 +232,10 @@
sprite: Clothing/Uniforms/Jumpskirt/prisoner.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpskirt/prisoner.rsi
+ - type: SuitSensor
+ controlsLocked: true
+ randomMode: false
+ mode: SensorCords
- type: entity
parent: ClothingUniformSkirtBase
diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
index 5894f40337..dd218fba7d 100644
--- a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
+++ b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
@@ -310,6 +310,10 @@
sprite: Clothing/Uniforms/Jumpsuit/prisoner.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/prisoner.rsi
+ - type: SuitSensor
+ controlsLocked: true
+ randomMode: false
+ mode: SensorCords
- type: entity
parent: ClothingUniformBase
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
index 2ffa85da9e..21cc3bd53a 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
@@ -35,6 +35,14 @@
- type: ComputerBoard
prototype: ComputerResearchAndDevelopment
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: CrewMonitoringComputerCircuitboard
+ name: crew monitoring console board
+ components:
+ - type: ComputerBoard
+ prototype: ComputerCrewMonitoring
+
- type: entity
parent: BaseComputerCircuitboard
id: IDComputerCircuitboard
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index 0be2035ed5..cba782735e 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -88,6 +88,36 @@
energy: 1.6
color: "#1f8c28"
+- type: entity
+ parent: ComputerBase
+ id: ComputerCrewMonitoring
+ name: crew monitoring console
+ description: Used to monitor active health sensors built into most of the crew's uniforms.
+ components:
+ - type: Appearance
+ visuals:
+ - type: ComputerVisualizer
+ key: med_key
+ screen: crew
+ - type: PointLight
+ radius: 1.5
+ energy: 1.6
+ color: "#006400"
+ - type: Computer
+ board: CrewMonitoringComputerCircuitboard
+ - type: ActivatableUI
+ key: enum.CrewMonitoringUIKey.Key
+ - type: ActivatableUIRequiresPower
+ - type: UserInterface
+ interfaces:
+ - key: enum.CrewMonitoringUIKey.Key
+ type: CrewMonitoringBoundUserInterface
+ - type: CrewMonitoringConsole
+ - type: DeviceNetworkComponent
+ deviceNetId: Wireless
+ - type: WirelessNetworkConnection
+ range: 500
+
- type: entity
parent: ComputerBase
id: ComputerResearchAndDevelopment
diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
index 03a5cb59fb..bb40207eec 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
@@ -183,6 +183,7 @@
- SolarControlComputerCircuitboard
- AutolatheMachineCircuitboard
- ProtolatheMachineCircuitboard
+ - CrewMonitoringComputerCircuitboard
- Bucket
- MopItem
- SprayBottle