]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict suitsensor system (#39325)
authorkosticia <kosticia46@gmail.com>
Mon, 11 Aug 2025 15:35:11 +0000 (18:35 +0300)
committerGitHub <noreply@github.com>
Mon, 11 Aug 2025 15:35:11 +0000 (17:35 +0200)
* adwadsdwasadwas

* dev

* fix

* review

* some more cleanup

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Content.Client/Medical/SuitSensors/SuitSensorSystem.cs [new file with mode: 0644]
Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
Content.Shared/Medical/SuitSensors/SharedSuitSensor.cs [moved from Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs with 100% similarity]
Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs [new file with mode: 0644]
Content.Shared/Medical/SuitSensors/SuitSensorComponent.cs [moved from Content.Server/Medical/SuitSensors/SuitSensorComponent.cs with 89% similarity]

diff --git a/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Client/Medical/SuitSensors/SuitSensorSystem.cs
new file mode 100644 (file)
index 0000000..75868e0
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Medical.SuitSensors;
+
+namespace Content.Client.Medical.SuitSensors;
+
+public sealed class SuitSensorSystem : SharedSuitSensorSystem;
index cb0fbd736c617f6aa787730e6c2c7e239255fb2e..7af093b17834abdedd63e3559c6f58650f5ddd53 100644 (file)
@@ -1,65 +1,25 @@
-using System.Numerics;
-using Content.Server.Access.Systems;
 using Content.Server.DeviceNetwork.Systems;
 using Content.Server.Emp;
 using Content.Server.Medical.CrewMonitoring;
-using Content.Server.Popups;
-using Content.Server.Station.Systems;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Clothing;
-using Content.Shared.Damage;
-using Content.Shared.DeviceNetwork;
-using Content.Shared.DoAfter;
-using Content.Shared.Examine;
-using Content.Shared.GameTicking;
-using Content.Shared.Interaction;
-using Content.Shared.Inventory;
+using Content.Shared.DeviceNetwork.Components;
 using Content.Shared.Medical.SuitSensor;
-using Content.Shared.Mobs;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Mobs.Systems;
-using Content.Shared.Verbs;
-using Robust.Shared.Containers;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
+using Content.Shared.Medical.SuitSensors;
 using Robust.Shared.Timing;
-using Content.Shared.DeviceNetwork.Components;
 
 namespace Content.Server.Medical.SuitSensors;
 
-public sealed class SuitSensorSystem : EntitySystem
+public sealed class SuitSensorSystem : SharedSuitSensorSystem
 {
     [Dependency] private readonly IGameTiming _gameTiming = default!;
-    [Dependency] private readonly IRobustRandom _random = 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!;
     [Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!;
-    [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
-    [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-    [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
-    [Dependency] private readonly IPrototypeManager _proto = default!;
-    [Dependency] private readonly InventorySystem _inventory = default!;
 
     public override void Initialize()
     {
         base.Initialize();
-        SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
-        SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
-        SubscribeLocalEvent<SuitSensorComponent, ClothingGotEquippedEvent>(OnEquipped);
-        SubscribeLocalEvent<SuitSensorComponent, ClothingGotUnequippedEvent>(OnUnequipped);
-        SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
-        SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
-        SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
-        SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
+
         SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
         SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemoved>(OnEmpFinished);
-        SubscribeLocalEvent<SuitSensorComponent, SuitSensorChangeDoAfterEvent>(OnSuitSensorDoAfter);
     }
 
     public override void Update(float frameTime)
@@ -78,14 +38,13 @@ public sealed class SuitSensorSystem : EntitySystem
             if (curTime < sensor.NextUpdate)
                 continue;
 
-            if (!CheckSensorAssignedStation(uid, sensor))
+            if (!CheckSensorAssignedStation((uid, sensor)))
                 continue;
 
-            // TODO: This would cause imprecision at different tick rates.
-            sensor.NextUpdate = curTime + sensor.UpdateRate;
+            sensor.NextUpdate += sensor.UpdateRate;
 
             // get sensor status
-            var status = GetSensorState(uid, sensor);
+            var status = GetSensorState((uid, sensor));
             if (status == null)
                 continue;
 
@@ -112,399 +71,21 @@ public sealed class SuitSensorSystem : EntitySystem
         }
     }
 
-    /// <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, 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, ref ClothingGotEquippedEvent args)
-    {
-        component.User = args.Wearer;
-    }
-
-    private void OnUnequipped(EntityUid uid, SuitSensorComponent component, ref ClothingGotUnequippedEvent args)
-    {
-        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.CanInteract || args.Hands == null)
-            return;
-
-        if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
-            return;
-
-        // check if target is incapacitated (cuffed, dead, etc)
-        if (component.User != null && args.User != component.User && _actionBlocker.CanInteract(component.User.Value, 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 void OnEmpPulse(EntityUid uid, SuitSensorComponent component, ref EmpPulseEvent args)
+    private void OnEmpPulse(Entity<SuitSensorComponent> ent, ref EmpPulseEvent args)
     {
         args.Affected = true;
         args.Disabled = true;
 
-        component.PreviousMode = component.Mode;
-        SetSensor((uid, component), SuitSensorMode.SensorOff, null);
-
-        component.PreviousControlsLocked = component.ControlsLocked;
-        component.ControlsLocked = true;
-    }
+        ent.Comp.PreviousMode = ent.Comp.Mode;
+        SetSensor(ent.AsNullable(), SuitSensorMode.SensorOff, null);
 
-    private void OnEmpFinished(EntityUid uid, SuitSensorComponent component, ref EmpDisabledRemoved args)
-    {
-        SetSensor((uid, component), component.PreviousMode, null);
-        component.ControlsLocked = component.PreviousControlsLocked;
+        ent.Comp.PreviousControlsLocked = ent.Comp.ControlsLocked;
+        ent.Comp.ControlsLocked = true;
     }
 
-    private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode)
+    private void OnEmpFinished(Entity<SuitSensorComponent> ent, ref EmpDisabledRemoved args)
     {
-        return new Verb()
-        {
-            Text = GetModeName(mode),
-            Disabled = component.Mode == mode,
-            Priority = -(int) mode, // sort them in descending order
-            Category = VerbCategory.SetSensor,
-            Act = () => TrySetSensor((uid, component), mode, userUid)
-        };
-    }
-
-    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 TrySetSensor(Entity<SuitSensorComponent> sensors, SuitSensorMode mode, EntityUid userUid)
-    {
-        var comp = sensors.Comp;
-
-        if (!Resolve(sensors, ref comp))
-            return;
-
-        if (comp.User == null || userUid == comp.User)
-            SetSensor(sensors, mode, userUid);
-        else
-        {
-            var doAfterEvent = new SuitSensorChangeDoAfterEvent(mode);
-            var doAfterArgs = new DoAfterArgs(EntityManager, userUid, comp.SensorsTime, doAfterEvent, sensors)
-            {
-                BreakOnMove = true,
-                BreakOnDamage = true
-            };
-
-            _doAfterSystem.TryStartDoAfter(doAfterArgs);
-        }
-    }
-
-    private void OnSuitSensorDoAfter(Entity<SuitSensorComponent> sensors, ref SuitSensorChangeDoAfterEvent args)
-    {
-        if (args.Handled || args.Cancelled)
-            return;
-
-        SetSensor(sensors, args.Mode, args.User);
-    }
-
-    public void SetSensor(Entity<SuitSensorComponent> sensors, SuitSensorMode mode, EntityUid? userUid = null)
-    {
-        var comp = sensors.Comp;
-
-        comp.Mode = mode;
-
-        if (userUid != null)
-        {
-            var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode)));
-            _popupSystem.PopupEntity(msg, sensors, userUid.Value);
-        }
-    }
-
-    /// <summary>
-    ///     Set all suit sensors on the equipment someone is wearing to the specified mode.
-    /// </summary>
-    public void SetAllSensors(EntityUid target, SuitSensorMode mode, SlotFlags slots = SlotFlags.All )
-    {
-        // iterate over all inventory slots
-        var slotEnumerator = _inventory.GetSlotEnumerator(target, slots);
-        while (slotEnumerator.NextItem(out var item, out _))
-        {
-            if (TryComp<SuitSensorComponent>(item, out var sensorComp))
-                SetSensor((item, sensorComp), mode);
-        }
-    }
-
-    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 || !HasComp<MobStateComponent>(sensor.User) || 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.LocalizedJobTitle != null)
-                userJob = card.Comp.LocalizedJobTitle;
-            userJobIcon = card.Comp.JobIcon;
-
-            foreach (var department in card.Comp.JobDepartments)
-                userJobDepartments.Add(Loc.GetString(_proto.Index(department).Name));
-        }
-
-        // get health mob state
-        var isAlive = false;
-        if (TryComp(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();
-
-        // Get mob total damage crit threshold
-        int? totalDamageThreshold = null;
-        if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, MobState.Critical, out var critThreshold))
-            totalDamageThreshold = critThreshold.Value.Int();
-
-        // finally, form suit sensor status
-        var status = new SuitSensorStatus(GetNetEntity(sensor.User.Value), 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;
-                status.TotalDamageThreshold = totalDamageThreshold;
-                break;
-            case SuitSensorMode.SensorCords:
-                status.IsAlive = isAlive;
-                status.TotalDamage = totalDamage;
-                status.TotalDamageThreshold = totalDamageThreshold;
-                EntityCoordinates coordinates;
-                var xformQuery = GetEntityQuery<TransformComponent>();
-
-                if (transform.GridUid != null)
-                {
-                    coordinates = new EntityCoordinates(transform.GridUid.Value,
-                        Vector2.Transform(_transform.GetWorldPosition(transform, xformQuery),
-                            _transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), 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,
-            [SuitSensorConstants.NET_OWNER_UID] = status.OwnerUid,
-        };
-
-        if (status.TotalDamage != null)
-            payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
-        if (status.TotalDamageThreshold != null)
-            payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, status.TotalDamageThreshold);
-        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;
-        if (!payload.TryGetValue(SuitSensorConstants.NET_OWNER_UID, out NetEntity ownerUid)) return null;
-
-        // try get total damage and cords (optionals)
-        payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
-        payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, out int? totalDamageThreshold);
-        payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords);
-
-        var status = new SuitSensorStatus(ownerUid, suitSensorUid, name, job, jobIcon, jobDepartments)
-        {
-            IsAlive = isAlive.Value,
-            TotalDamage = totalDamage,
-            TotalDamageThreshold = totalDamageThreshold,
-            Coordinates = coords,
-        };
-        return status;
+        SetSensor(ent.AsNullable(), ent.Comp.PreviousMode, null);
+        ent.Comp.ControlsLocked = ent.Comp.PreviousControlsLocked;
     }
 }
diff --git a/Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs b/Content.Shared/Medical/SuitSensors/SharedSuitSensorSystem.cs
new file mode 100644 (file)
index 0000000..2ed1089
--- /dev/null
@@ -0,0 +1,468 @@
+using System.Numerics;
+using Content.Shared.Access.Systems;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Clothing;
+using Content.Shared.Damage;
+using Content.Shared.DeviceNetwork;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.GameTicking;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Shared.Medical.SuitSensor;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Station;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Medical.SuitSensors;
+
+public abstract class SharedSuitSensorSystem : EntitySystem
+{
+    [Dependency] private readonly SharedStationSystem _stationSystem = default!;
+    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+    [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
+    [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly InventorySystem _inventory = default!;
+    [Dependency] private readonly SharedIdCardSystem _idCardSystem = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    private EntityQuery<SuitSensorComponent> _sensorQuery;
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
+        SubscribeLocalEvent<SuitSensorComponent, ClothingGotEquippedEvent>(OnEquipped);
+        SubscribeLocalEvent<SuitSensorComponent, ClothingGotUnequippedEvent>(OnUnequipped);
+        SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
+        SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
+        SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
+        SubscribeLocalEvent<SuitSensorComponent, SuitSensorChangeDoAfterEvent>(OnSuitSensorDoAfter);
+
+        _sensorQuery = GetEntityQuery<SuitSensorComponent>();
+    }
+
+    /// <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>
+    public bool CheckSensorAssignedStation(Entity<SuitSensorComponent> sensor)
+    {
+        if (!sensor.Comp.StationId.HasValue && Transform(sensor.Owner).GridUid == null)
+            return false;
+
+        sensor.Comp.StationId = _stationSystem.GetOwningStation(sensor.Owner);
+        Dirty(sensor);
+        return sensor.Comp.StationId.HasValue;
+    }
+
+    private void OnMapInit(Entity<SuitSensorComponent> ent, ref MapInitEvent args)
+    {
+        // Fallback
+        ent.Comp.StationId ??= _stationSystem.GetOwningStation(ent.Owner);
+
+        // generate random mode
+        if (ent.Comp.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
+            };
+            ent.Comp.Mode = _random.Pick(modesDist);
+        }
+
+        ent.Comp.NextUpdate = _timing.CurTime;
+        Dirty(ent);
+    }
+
+    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.
+        RecursiveSensor(ev.Mob, ev.Station);
+    }
+
+    private void RecursiveSensor(EntityUid uid, EntityUid stationUid)
+    {
+        var xform = Transform(uid);
+        var enumerator = xform.ChildEnumerator;
+
+        while (enumerator.MoveNext(out var child))
+        {
+            if (_sensorQuery.TryComp(child, out var sensor))
+            {
+                sensor.StationId = stationUid;
+                Dirty(child, sensor);
+            }
+
+            RecursiveSensor(child, stationUid);
+        }
+    }
+
+    private void OnEquipped(Entity<SuitSensorComponent> ent, ref ClothingGotEquippedEvent args)
+    {
+        ent.Comp.User = args.Wearer;
+        Dirty(ent);
+    }
+
+    private void OnUnequipped(Entity<SuitSensorComponent> ent, ref ClothingGotUnequippedEvent args)
+    {
+        ent.Comp.User = null;
+        Dirty(ent);
+    }
+
+    private void OnExamine(Entity<SuitSensorComponent> ent, ref ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        string msg;
+        switch (ent.Comp.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(Entity<SuitSensorComponent> ent, ref GetVerbsEvent<Verb> args)
+    {
+        // check if user can change sensor
+        if (ent.Comp.ControlsLocked)
+            return;
+
+        // standard interaction checks
+        if (!args.CanInteract || args.Hands == null)
+            return;
+
+        if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
+            return;
+
+        // check if target is incapacitated (cuffed, dead, etc)
+        if (ent.Comp.User != null && args.User != ent.Comp.User && _actionBlocker.CanInteract(ent.Comp.User.Value, null))
+            return;
+
+        args.Verbs.UnionWith(new[]
+        {
+            CreateVerb(ent, args.User, SuitSensorMode.SensorOff),
+            CreateVerb(ent, args.User, SuitSensorMode.SensorBinary),
+            CreateVerb(ent, args.User, SuitSensorMode.SensorVitals),
+            CreateVerb(ent, args.User, SuitSensorMode.SensorCords)
+        });
+    }
+
+    private void OnInsert(Entity<SuitSensorComponent> ent, ref EntGotInsertedIntoContainerMessage args)
+    {
+        if (args.Container.ID != ent.Comp.ActivationContainer)
+            return;
+
+        ent.Comp.User = args.Container.Owner;
+        Dirty(ent);
+    }
+
+    private void OnRemove(Entity<SuitSensorComponent> ent, ref EntGotRemovedFromContainerMessage args)
+    {
+        if (args.Container.ID != ent.Comp.ActivationContainer)
+            return;
+
+        ent.Comp.User = null;
+        Dirty(ent);
+    }
+
+    private Verb CreateVerb(Entity<SuitSensorComponent> ent, EntityUid userUid, SuitSensorMode mode)
+    {
+        return new Verb()
+        {
+            Text = GetModeName(mode),
+            Disabled = ent.Comp.Mode == mode,
+            Priority = -(int)mode, // sort them in descending order
+            Category = VerbCategory.SetSensor,
+            Act = () => TrySetSensor(ent.AsNullable(), mode, userUid)
+        };
+    }
+
+    public 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);
+    }
+
+    /// <summary>
+    /// Attempts to set <see cref="SuitSensorComponent"/> mode of the entity to the selected in params.
+    /// Works instantly if the user is the player wearing the sensors and will start a DoAfter otherwise.
+    /// </summary>
+    /// <param name="sensors">Entity and its component that should be changed.</param>
+    /// <param name="mode">Selected mode</param>
+    /// <param name="userUid">userUid, when not equal to the <see cref="SuitSensorComponent.User"/>, creates doafter</param>
+    public bool TrySetSensor(Entity<SuitSensorComponent?> sensors, SuitSensorMode mode, EntityUid userUid)
+    {
+        if (!Resolve(sensors, ref sensors.Comp, false))
+            return false;
+
+        if (sensors.Comp.User == null || userUid == sensors.Comp.User)
+            SetSensor(sensors, mode, userUid);
+        else
+        {
+            var doAfterEvent = new SuitSensorChangeDoAfterEvent(mode);
+            var doAfterArgs = new DoAfterArgs(EntityManager, userUid, sensors.Comp.SensorsTime, doAfterEvent, sensors)
+            {
+                BreakOnMove = true,
+                BreakOnDamage = true
+            };
+
+            _doAfterSystem.TryStartDoAfter(doAfterArgs);
+        }
+        return true;
+    }
+
+    private void OnSuitSensorDoAfter(Entity<SuitSensorComponent> sensors, ref SuitSensorChangeDoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled)
+            return;
+
+        SetSensor(sensors.AsNullable(), args.Mode, args.User);
+    }
+
+    /// <summary>
+    /// Sets mode of the <see cref="SuitSensorComponent"/> of the chosen entity.
+    /// Makes popup when <param name="userUid"> not null
+    /// </summary>
+    /// <param name="sensors">Entity and it's component that should be changed</param>
+    /// <param name="mode">Selected mode</param>
+    /// <param name="userUid">uid, required for the popup</param>
+    public void SetSensor(Entity<SuitSensorComponent?> sensors, SuitSensorMode mode, EntityUid? userUid = null)
+    {
+        if (!Resolve(sensors, ref sensors.Comp, false))
+            return;
+
+        sensors.Comp.Mode = mode;
+        Dirty(sensors);
+
+        if (userUid != null)
+        {
+            var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode)));
+            _popupSystem.PopupClient(msg, sensors, userUid.Value);
+        }
+    }
+
+    /// <summary>
+    /// Set all suit sensors on the equipment someone is wearing to the specified mode.
+    /// </summary>
+    public void SetAllSensors(EntityUid target, SuitSensorMode mode, SlotFlags slots = SlotFlags.All)
+    {
+        // iterate over all inventory slots
+        var slotEnumerator = _inventory.GetSlotEnumerator(target, slots);
+        while (slotEnumerator.NextItem(out var item, out _))
+        {
+            if (TryComp<SuitSensorComponent>(item, out var sensorComp))
+                SetSensor((item, sensorComp), mode);
+        }
+    }
+
+    /// <summary>
+    /// Attempts to get full <see cref="SuitSensorStatus"/> from the <see cref="SuitSensorComponent"/>
+    /// </summary>
+    /// <param name="uid">Entity to get status</param>
+    /// <returns>Full <see cref="SuitSensorStatus"/> of the chosen uid</returns>
+    public SuitSensorStatus? GetSensorState(Entity<SuitSensorComponent?, TransformComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
+            return null;
+
+        var sensor = ent.Comp1;
+        var transform = ent.Comp2;
+
+        // check if sensor is enabled and worn by user
+        if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || !HasComp<MobStateComponent>(sensor.User) || 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.LocalizedJobTitle != null)
+                userJob = card.Comp.LocalizedJobTitle;
+            userJobIcon = card.Comp.JobIcon;
+
+            foreach (var department in card.Comp.JobDepartments)
+                userJobDepartments.Add(Loc.GetString(_proto.Index(department).Name));
+        }
+
+        // get health mob state
+        var isAlive = false;
+        if (TryComp(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();
+
+        // Get mob total damage crit threshold
+        int? totalDamageThreshold = null;
+        if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, MobState.Critical, out var critThreshold))
+            totalDamageThreshold = critThreshold.Value.Int();
+
+        // finally, form suit sensor status
+        var status = new SuitSensorStatus(GetNetEntity(sensor.User.Value), GetNetEntity(ent.Owner), userName, userJob, userJobIcon, userJobDepartments);
+        switch (sensor.Mode)
+        {
+            case SuitSensorMode.SensorBinary:
+                status.IsAlive = isAlive;
+                break;
+            case SuitSensorMode.SensorVitals:
+                status.IsAlive = isAlive;
+                status.TotalDamage = totalDamage;
+                status.TotalDamageThreshold = totalDamageThreshold;
+                break;
+            case SuitSensorMode.SensorCords:
+                status.IsAlive = isAlive;
+                status.TotalDamage = totalDamage;
+                status.TotalDamageThreshold = totalDamageThreshold;
+                EntityCoordinates coordinates;
+                var xformQuery = GetEntityQuery<TransformComponent>();
+
+                if (transform.GridUid != null)
+                {
+                    coordinates = new EntityCoordinates(transform.GridUid.Value,
+                        Vector2.Transform(_transform.GetWorldPosition(transform, xformQuery),
+                            _transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), 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>
+    /// 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,
+            [SuitSensorConstants.NET_OWNER_UID] = status.OwnerUid,
+        };
+
+        if (status.TotalDamage != null)
+            payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
+        if (status.TotalDamageThreshold != null)
+            payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, status.TotalDamageThreshold);
+        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;
+        if (!payload.TryGetValue(SuitSensorConstants.NET_OWNER_UID, out NetEntity ownerUid)) return null;
+
+        // try get total damage and cords (optionals)
+        payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
+        payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, out int? totalDamageThreshold);
+        payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords);
+
+        var status = new SuitSensorStatus(ownerUid, suitSensorUid, name, job, jobIcon, jobDepartments)
+        {
+            IsAlive = isAlive.Value,
+            TotalDamage = totalDamage,
+            TotalDamageThreshold = totalDamageThreshold,
+            Coordinates = coords,
+        };
+        return status;
+    }
+}
similarity index 89%
rename from Content.Server/Medical/SuitSensors/SuitSensorComponent.cs
rename to Content.Shared/Medical/SuitSensors/SuitSensorComponent.cs
index 91039712e578343174111a6f1ad1761fac0dff7f..b20b7af2c9e4b922d89537d314b66d11c5a76959 100644 (file)
@@ -1,14 +1,16 @@
 using Content.Shared.Medical.SuitSensor;
+using Robust.Shared.GameStates;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
-namespace Content.Server.Medical.SuitSensors;
+namespace Content.Shared.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, AutoGenerateComponentPause]
-[Access(typeof(SuitSensorSystem))]
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedSuitSensorSystem))]
+[AutoGenerateComponentState, AutoGenerateComponentPause]
 public sealed partial class SuitSensorComponent : Component
 {
     /// <summary>
@@ -20,7 +22,7 @@ public sealed partial class SuitSensorComponent : Component
     /// <summary>
     ///     If true user can't change suit sensor mode
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool ControlsLocked = false;
 
     /// <summary>
@@ -32,7 +34,7 @@ public sealed partial class SuitSensorComponent : Component
     /// <summary>
     ///     Current sensor mode. Can be switched by user verbs.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public SuitSensorMode Mode = SuitSensorMode.SensorOff;
 
     /// <summary>
@@ -56,7 +58,7 @@ public sealed partial class SuitSensorComponent : Component
     /// <summary>
     ///     Current user that wears suit sensor. Null if nobody wearing it.
     /// </summary>
-    [ViewVariables]
+    [DataField, AutoNetworkedField]
     public EntityUid? User = null;
 
     /// <summary>
@@ -69,7 +71,7 @@ public sealed partial class SuitSensorComponent : Component
     /// <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.
     /// </summary>
-    [DataField("station")]
+    [DataField("station"), AutoNetworkedField]
     public EntityUid? StationId = null;
 
     /// <summary>