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