From 6567fa36e468ca0b2091a862609ee82b77f3d277 Mon Sep 17 00:00:00 2001
From: to4no_fix <156101927+chavonadelal@users.noreply.github.com>
Date: Thu, 15 Aug 2024 17:30:39 +0300
Subject: [PATCH 1/5] Adding shock collar and electropack (#30529)

* Adding shock collar with the new ShockOnTrigger

* Cleaning and updating the shock collar

* Add StripDelay datafield to ClothingComponent

* Adding SelfUnremovableClothingComponent

* ShockCollar Update

* Correction of the shock collar

* Correction of the shock collar 2

* Renaming the DamageSpecifier DataField to Damage

* Fixing the damage field in ShockCollar

* Cleaning the ShockCollar

* Renaming ShockCollar to ClothingNeckShockCollar

* Adding ClothingNeckShockCollar as a stealTarget to a thief

* Fixing a typo of the sprite path in ClothingNeckShockCollar

* Cleaning the ShockOnTriggerComponent

* Revision of SelfUnremovableClothing

* Adding a ClothingBackpackElectropack

* Sprite fix

* Code review

* Shock Collar sprite update

* add commit hash

---------

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
---
 .../Components/ShockOnTriggerComponent.cs     |  37 ++++++++++++++++++
 .../Explosion/EntitySystems/TriggerSystem.cs  |  22 +++++++++++
 Content.Server/Strip/StrippableSystem.cs      |   8 ++--
 .../Clothing/Components/ClothingComponent.cs  |   7 ++++
 .../SelfUnremovableClothingComponent.cs       |  18 +++++++++
 .../Clothing/EntitySystems/ClothingSystem.cs  |   8 ++++
 .../SelfUnremovableClothingSystem.cs          |  36 +++++++++++++++++
 .../EntitySystems/ToggleableClothingSystem.cs |   2 +-
 .../Strip/Components/StrippableComponent.cs   |   9 +++++
 .../Strip/SharedStrippableSystem.cs           |  16 +++++---
 .../self-unremovable-clothing-component.ftl   |   1 +
 .../Locale/en-US/research/technologies.ftl    |   1 +
 .../Catalog/Fills/Lockers/security.yml        |   8 ++++
 .../Entities/Clothing/Back/backpacks.yml      |  23 +++++++++++
 .../Entities/Objects/Devices/shock_collar.yml |  36 +++++++++++++++++
 .../Entities/Structures/Machines/lathe.yml    |   3 +-
 .../Prototypes/Objectives/objectiveGroups.yml |   1 +
 .../Objectives/stealTargetGroups.yml          |   7 ++++
 Resources/Prototypes/Objectives/thief.yml     |  11 ++++++
 .../Prototypes/Recipes/Lathes/security.yml    |  19 ++++++---
 Resources/Prototypes/Research/arsenal.yml     |  22 ++++++++---
 .../electropack.rsi/equipped-BACKPACK.png     | Bin 0 -> 594 bytes
 .../Back/Backpacks/electropack.rsi/icon.png   | Bin 0 -> 457 bytes
 .../Backpacks/electropack.rsi/inhand-left.png | Bin 0 -> 432 bytes
 .../electropack.rsi/inhand-right.png          | Bin 0 -> 441 bytes
 .../Back/Backpacks/electropack.rsi/meta.json  |  33 ++++++++++++++++
 .../Misc/shock_collar.rsi/equipped-NECK.png   | Bin 0 -> 465 bytes
 .../Neck/Misc/shock_collar.rsi/icon.png       | Bin 0 -> 800 bytes
 .../Neck/Misc/shock_collar.rsi/meta.json      |  19 +++++++++
 29 files changed, 326 insertions(+), 21 deletions(-)
 create mode 100644 Content.Server/Explosion/Components/ShockOnTriggerComponent.cs
 create mode 100644 Content.Shared/Clothing/Components/SelfUnremovableClothingComponent.cs
 create mode 100644 Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs
 create mode 100644 Resources/Locale/en-US/clothing/components/self-unremovable-clothing-component.ftl
 create mode 100644 Resources/Prototypes/Entities/Objects/Devices/shock_collar.yml
 create mode 100644 Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/equipped-BACKPACK.png
 create mode 100644 Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/icon.png
 create mode 100644 Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-left.png
 create mode 100644 Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-right.png
 create mode 100644 Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/meta.json
 create mode 100644 Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/equipped-NECK.png
 create mode 100644 Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/icon.png
 create mode 100644 Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/meta.json

diff --git a/Content.Server/Explosion/Components/ShockOnTriggerComponent.cs b/Content.Server/Explosion/Components/ShockOnTriggerComponent.cs
new file mode 100644
index 0000000000..a553cc047a
--- /dev/null
+++ b/Content.Server/Explosion/Components/ShockOnTriggerComponent.cs
@@ -0,0 +1,37 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Content.Server.Explosion.EntitySystems;
+
+namespace Content.Server.Explosion.Components;
+
+/// <summary>
+/// A component that electrocutes an entity having this component when a trigger is triggered.
+/// </summary>
+[RegisterComponent, AutoGenerateComponentPause]
+[Access(typeof(TriggerSystem))]
+public sealed partial class ShockOnTriggerComponent : Component
+{
+    /// <summary>
+    /// The force of an electric shock when the trigger is triggered.
+    /// </summary>
+    [DataField]
+    public int Damage = 5;
+
+    /// <summary>
+    /// Duration of electric shock when the trigger is triggered.
+    /// </summary>
+    [DataField]
+    public TimeSpan Duration = TimeSpan.FromSeconds(2);
+
+    /// <summary>
+    /// The minimum delay between repeating triggers.
+    /// </summary>
+    [DataField]
+    public TimeSpan Cooldown = TimeSpan.FromSeconds(4);
+
+    /// <summary>
+    /// When can the trigger run again?
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    [AutoPausedField]
+    public TimeSpan NextTrigger = TimeSpan.Zero;
+}
diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs
index 92e065bf4c..1208cd1771 100644
--- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs
+++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs
@@ -3,6 +3,7 @@
 using Content.Server.Chemistry.Containers.EntitySystems;
 using Content.Server.Explosion.Components;
 using Content.Server.Flash;
+using Content.Server.Electrocution;
 using Content.Server.Pinpointer;
 using Content.Shared.Flash.Components;
 using Content.Server.Radio.EntitySystems;
@@ -33,6 +34,7 @@
 using Robust.Shared.Player;
 using Content.Shared.Coordinates;
 using Robust.Shared.Utility;
+using Robust.Shared.Timing;
 
 namespace Content.Server.Explosion.EntitySystems
 {
@@ -75,6 +77,7 @@ public sealed partial class TriggerSystem : EntitySystem
         [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
         [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
         [Dependency] private readonly InventorySystem _inventory = default!;
+        [Dependency] private readonly ElectrocutionSystem _electrocution = default!;
 
         public override void Initialize()
         {
@@ -104,6 +107,7 @@ public override void Initialize()
 
             SubscribeLocalEvent<AnchorOnTriggerComponent, TriggerEvent>(OnAnchorTrigger);
             SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
+            SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
             SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
         }
 
@@ -120,6 +124,24 @@ private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, Tr
             }
         }
 
+        private void HandleShockTrigger(Entity<ShockOnTriggerComponent> shockOnTrigger, ref TriggerEvent args)
+        {
+            if (!_container.TryGetContainingContainer(shockOnTrigger, out var container))
+                return;
+
+            var containerEnt = container.Owner;
+            var curTime = _timing.CurTime;
+
+            if (curTime < shockOnTrigger.Comp.NextTrigger)
+            {
+                // The trigger's on cooldown.
+                return;
+            }
+
+            _electrocution.TryDoElectrocution(containerEnt, null, shockOnTrigger.Comp.Damage, shockOnTrigger.Comp.Duration, true);
+            shockOnTrigger.Comp.NextTrigger = curTime + shockOnTrigger.Comp.Cooldown;
+        }
+
         private void OnAnchorTrigger(EntityUid uid, AnchorOnTriggerComponent component, TriggerEvent args)
         {
             var xform = Transform(uid);
diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs
index 194df7b3d0..6d728df9d6 100644
--- a/Content.Server/Strip/StrippableSystem.cs
+++ b/Content.Server/Strip/StrippableSystem.cs
@@ -218,7 +218,7 @@ private void StartStripInsertInventory(
                 return;
             }
 
-            var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime);
+            var (time, stealth) = GetStripTimeModifiers(user, target, held, slotDef.StripTime);
 
             if (!stealth)
                 _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
@@ -306,7 +306,7 @@ private void StartStripRemoveInventory(
                 return;
             }
 
-            var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime);
+            var (time, stealth) = GetStripTimeModifiers(user, target, item, slotDef.StripTime);
 
             if (!stealth)
             {
@@ -411,7 +411,7 @@ private void StartStripInsertHand(
             if (!CanStripInsertHand(user, target, held, handName))
                 return;
 
-            var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
+            var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
 
             if (!stealth)
                 _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert-hand", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
@@ -510,7 +510,7 @@ private void StartStripRemoveHand(
             if (!CanStripRemoveHand(user, target, item, handName))
                 return;
 
-            var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
+            var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
 
             if (!stealth)
                 _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target);
diff --git a/Content.Shared/Clothing/Components/ClothingComponent.cs b/Content.Shared/Clothing/Components/ClothingComponent.cs
index 581125d4fe..4f8058dbf5 100644
--- a/Content.Shared/Clothing/Components/ClothingComponent.cs
+++ b/Content.Shared/Clothing/Components/ClothingComponent.cs
@@ -69,6 +69,13 @@ public sealed partial class ClothingComponent : Component
 
     [DataField, ViewVariables(VVAccess.ReadWrite)]
     public TimeSpan UnequipDelay = TimeSpan.Zero;
+
+    /// <summary>
+    /// Offset for the strip time for an entity with this component.
+    /// Only applied when it is being equipped or removed by another player.
+    /// </summary>
+    [DataField]
+    public TimeSpan StripDelay = TimeSpan.Zero;
 }
 
 [Serializable, NetSerializable]
diff --git a/Content.Shared/Clothing/Components/SelfUnremovableClothingComponent.cs b/Content.Shared/Clothing/Components/SelfUnremovableClothingComponent.cs
new file mode 100644
index 0000000000..1d624516ec
--- /dev/null
+++ b/Content.Shared/Clothing/Components/SelfUnremovableClothingComponent.cs
@@ -0,0 +1,18 @@
+using Content.Shared.Clothing.EntitySystems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Clothing.Components;
+
+/// <summary>
+///     The component prohibits the player from taking off clothes on them that have this component.
+/// </summary>
+/// <remarks>
+///     See also ClothingComponent.EquipDelay if you want the clothes that the player cannot take off by himself to be put on by the player with a delay.
+///</remarks>
+[NetworkedComponent]
+[RegisterComponent]
+[Access(typeof(SelfUnremovableClothingSystem))]
+public sealed partial class SelfUnremovableClothingComponent : Component
+{
+
+}
diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs
index 082b040a32..3b26360f10 100644
--- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs
+++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs
@@ -6,6 +6,7 @@
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Item;
+using Content.Shared.Strip.Components;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 
@@ -32,6 +33,8 @@ public override void Initialize()
 
         SubscribeLocalEvent<ClothingComponent, ClothingEquipDoAfterEvent>(OnEquipDoAfter);
         SubscribeLocalEvent<ClothingComponent, ClothingUnequipDoAfterEvent>(OnUnequipDoAfter);
+
+        SubscribeLocalEvent<ClothingComponent, BeforeItemStrippedEvent>(OnItemStripped);
     }
 
     private void OnUseInHand(Entity<ClothingComponent> ent, ref UseInHandEvent args)
@@ -192,6 +195,11 @@ private void OnUnequipDoAfter(Entity<ClothingComponent> ent, ref ClothingUnequip
             _handsSystem.TryPickup(args.User, ent);
     }
 
+    private void OnItemStripped(Entity<ClothingComponent> ent, ref BeforeItemStrippedEvent args)
+    {
+        args.Additive += ent.Comp.StripDelay;
+    }
+
     private void CheckEquipmentForLayerHide(EntityUid equipment, EntityUid equipee)
     {
         if (TryComp(equipment, out HideLayerClothingComponent? clothesComp) && TryComp(equipee, out HumanoidAppearanceComponent? appearanceComp))
diff --git a/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs
new file mode 100644
index 0000000000..ab0c41c5c7
--- /dev/null
+++ b/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs
@@ -0,0 +1,36 @@
+using Content.Shared.Clothing.Components;
+using Content.Shared.Examine;
+using Content.Shared.Inventory;
+using Content.Shared.Inventory.Events;
+
+namespace Content.Shared.Clothing.EntitySystems;
+
+/// <summary>
+///     A system for the operation of a component that prohibits the player from taking off his own clothes that have this component.
+/// </summary>
+public sealed class SelfUnremovableClothingSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<SelfUnremovableClothingComponent, BeingUnequippedAttemptEvent>(OnUnequip);
+        SubscribeLocalEvent<SelfUnremovableClothingComponent, ExaminedEvent>(OnUnequipMarkup);
+    }
+
+    private void OnUnequip(Entity<SelfUnremovableClothingComponent> selfUnremovableClothing, ref BeingUnequippedAttemptEvent args)
+    {
+        if (TryComp<ClothingComponent>(selfUnremovableClothing, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE)
+            return;
+
+        if (args.UnEquipTarget == args.Unequipee)
+        {
+            args.Cancel();
+        }
+    }
+
+    private void OnUnequipMarkup(Entity<SelfUnremovableClothingComponent> selfUnremovableClothing, ref ExaminedEvent args)
+    {
+        args.PushMarkup(Loc.GetString("comp-self-unremovable-clothing"));
+    }
+}
diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
index c828b22481..aa3381c6bf 100644
--- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
+++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
@@ -95,7 +95,7 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg
         if (component.StripDelay == null)
             return;
 
-        var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value);
+        var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, item, component.StripDelay.Value);
 
         var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item)
         {
diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs
index 4faca4d8f2..5191b3f3f9 100644
--- a/Content.Shared/Strip/Components/StrippableComponent.cs
+++ b/Content.Shared/Strip/Components/StrippableComponent.cs
@@ -44,6 +44,15 @@ public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth =
         public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES;
     }
 
+    /// <summary>
+    ///     Used to modify strip times. Raised directed at the item being stripped.
+    /// </summary>
+    /// <remarks>
+    ///     This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player.
+    /// </remarks>
+    [ByRefEvent]
+    public sealed class BeforeItemStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth);
+
     /// <summary>
     ///     Used to modify strip times. Raised directed at the user.
     /// </summary>
diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs
index e42f6e3aa7..935dc33540 100644
--- a/Content.Shared/Strip/SharedStrippableSystem.cs
+++ b/Content.Shared/Strip/SharedStrippableSystem.cs
@@ -28,13 +28,19 @@ private void OnActivateInWorld(EntityUid uid, StrippableComponent component, Act
             args.Handled = true;
     }
 
-    public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime)
+    /// <summary>
+    /// Modify the strip time via events. Raised directed at the item being stripped, the player stripping someone and the player being stripped.
+    /// </summary>
+    public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid targetPlayer, EntityUid? targetItem, TimeSpan initialTime)
     {
-        var userEv = new BeforeStripEvent(initialTime);
+        var itemEv = new BeforeItemStrippedEvent(initialTime, false);
+        if (targetItem != null)
+            RaiseLocalEvent(targetItem.Value, ref itemEv);
+        var userEv = new BeforeStripEvent(itemEv.Time, itemEv.Stealth);
         RaiseLocalEvent(user, ref userEv);
-        var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
-        RaiseLocalEvent(target, ref ev);
-        return (ev.Time, ev.Stealth);
+        var targetEv = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
+        RaiseLocalEvent(targetPlayer, ref targetEv);
+        return (targetEv.Time, targetEv.Stealth);
     }
 
     private void OnDragDrop(EntityUid uid, StrippableComponent component, ref DragDropDraggedEvent args)
diff --git a/Resources/Locale/en-US/clothing/components/self-unremovable-clothing-component.ftl b/Resources/Locale/en-US/clothing/components/self-unremovable-clothing-component.ftl
new file mode 100644
index 0000000000..bb7ff0206f
--- /dev/null
+++ b/Resources/Locale/en-US/clothing/components/self-unremovable-clothing-component.ftl
@@ -0,0 +1 @@
+comp-self-unremovable-clothing = This cannot be removed without outside help.
diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl
index 4fbb0e1bd3..0b0970ec08 100644
--- a/Resources/Locale/en-US/research/technologies.ftl
+++ b/Resources/Locale/en-US/research/technologies.ftl
@@ -26,6 +26,7 @@ research-technology-salvage-weapons = Salvage Weapons
 research-technology-draconic-munitions = Draconic Munitions
 research-technology-uranium-munitions = Uranium Munitions
 research-technology-explosive-technology = Explosive Technology
+research-technology-special-means = Special Means
 research-technology-weaponized-laser-manipulation = Weaponized Laser Manipulation
 research-technology-nonlethal-ammunition = Nonlethal Ammunition
 research-technology-practice-ammunition = Practice Ammunition
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
index 2d012128e6..b72fce1107 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
@@ -20,6 +20,10 @@
       - id: ClothingOuterHardsuitWarden
       - id: HoloprojectorSecurity
       - id: BookSpaceLaw
+      - id: ClothingNeckShockCollar
+        amount: 2
+      - id: RemoteSignaller
+        amount: 2
 
 - type: entity
   id: LockerWardenFilled
@@ -42,6 +46,10 @@
       - id: DoorRemoteArmory
       - id: HoloprojectorSecurity
       - id: BookSpaceLaw
+      - id: ClothingNeckShockCollar
+        amount: 2
+      - id: RemoteSignaller
+        amount: 2
 
 - type: entity
   id: LockerSecurityFilled
diff --git a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml
index 2d5bf42466..f94b773886 100644
--- a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml
+++ b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml
@@ -314,6 +314,29 @@
   - type: Unremoveable
     deleteOnDrop: false
 
+- type: entity
+  parent: ClothingBackpack
+  id: ClothingBackpackElectropack
+  name: electropack
+  suffix: SelfUnremovable
+  description: Shocks on the signal. It is used to keep a particularly dangerous criminal under control.
+  components:
+  - type: Sprite
+    sprite: Clothing/Back/Backpacks/electropack.rsi
+    state: icon
+  - type: Clothing
+    stripDelay: 10
+    equipDelay: 5 # to avoid accidentally falling into the trap associated with SelfUnremovableClothing
+  - type: SelfUnremovableClothing
+  - type: ShockOnTrigger
+    damage: 5
+    duration: 3
+    cooldown: 4
+  - type: TriggerOnSignal
+  - type: DeviceLinkSink
+    ports:
+      - Trigger
+
 # Debug
 - type: entity
   parent: ClothingBackpack
diff --git a/Resources/Prototypes/Entities/Objects/Devices/shock_collar.yml b/Resources/Prototypes/Entities/Objects/Devices/shock_collar.yml
new file mode 100644
index 0000000000..22f2d097fc
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Devices/shock_collar.yml
@@ -0,0 +1,36 @@
+- type: entity
+  parent: Clothing
+  id: ClothingNeckShockCollar
+  name: shock collar
+  suffix: SelfUnremovable
+  description: An electric collar that shocks on the signal.
+  components:
+  - type: Item
+    size: Small
+  - type: Sprite
+    sprite: Clothing/Neck/Misc/shock_collar.rsi
+    state: icon
+  - type: Clothing
+    sprite: Clothing/Neck/Misc/shock_collar.rsi
+    stripDelay: 10
+    equipDelay: 5 # to avoid accidentally falling into the trap associated with SelfUnremovableClothing
+    quickEquip: true
+    slots:
+    - neck
+  - type: SelfUnremovableClothing
+  - type: ShockOnTrigger
+    damage: 5
+    duration: 3
+    cooldown: 4
+  - type: TriggerOnSignal
+  - type: DeviceLinkSink
+    ports:
+      - Trigger
+  - type: GuideHelp
+    guides:
+      - Security
+  - type: StealTarget
+    stealGroup: ClothingNeckShockCollar
+  - type: Tag
+    tags:
+    - WhitelistChameleon
diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
index 98d5440e3e..e795a5836e 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
@@ -16,7 +16,7 @@
         mask:
         - MachineMask
         layer:
-          - MachineLayer  
+          - MachineLayer
   - type: Lathe
   - type: MaterialStorage
   - type: Destructible
@@ -808,6 +808,7 @@
       - WeaponLaserCannon
       - WeaponLaserCarbine
       - WeaponXrayCannon
+      - ClothingBackpackElectropack
   - type: MaterialStorage
     whitelist:
       tags:
diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml
index e62aa9fdf6..c692c85dff 100644
--- a/Resources/Prototypes/Objectives/objectiveGroups.yml
+++ b/Resources/Prototypes/Objectives/objectiveGroups.yml
@@ -73,6 +73,7 @@
     ForensicScannerStealObjective: 1                    #sec
     FlippoEngravedLighterStealObjective: 0.5
     ClothingHeadHatWardenStealObjective: 1
+    ClothingNeckShockCollarStealObjective: 1
     ClothingOuterHardsuitVoidParamedStealObjective: 1   #med
     MedicalTechFabCircuitboardStealObjective: 1
     ClothingHeadsetAltMedicalStealObjective: 1
diff --git a/Resources/Prototypes/Objectives/stealTargetGroups.yml b/Resources/Prototypes/Objectives/stealTargetGroups.yml
index 1a9b4223cb..e818442b4c 100644
--- a/Resources/Prototypes/Objectives/stealTargetGroups.yml
+++ b/Resources/Prototypes/Objectives/stealTargetGroups.yml
@@ -272,6 +272,13 @@
     sprite: Clothing/Neck/Medals/clownmedal.rsi
     state: icon
 
+- type: stealTargetGroup
+  id: ClothingNeckShockCollar
+  name: shock collar
+  sprite:
+    sprite: Clothing/Neck/Misc/shock_collar.rsi
+    state: icon
+
 #Thief structures
 
 - type: stealTargetGroup
diff --git a/Resources/Prototypes/Objectives/thief.yml b/Resources/Prototypes/Objectives/thief.yml
index 672f9b2ba7..092a724da2 100644
--- a/Resources/Prototypes/Objectives/thief.yml
+++ b/Resources/Prototypes/Objectives/thief.yml
@@ -316,6 +316,17 @@
   - type: Objective
     difficulty: 1
 
+- type: entity
+  parent: BaseThiefStealObjective
+  id: ClothingNeckShockCollarStealObjective
+  components:
+  - type: NotJobRequirement
+    job: Warden
+  - type: StealCondition
+    stealGroup: ClothingNeckShockCollar
+  - type: Objective
+    difficulty: 1
+
 # Structures
 
 - type: entity
diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml
index 1e6b70f943..a54d5b6235 100644
--- a/Resources/Prototypes/Recipes/Lathes/security.yml
+++ b/Resources/Prototypes/Recipes/Lathes/security.yml
@@ -38,7 +38,7 @@
   materials:
     Steel: 250
     Plastic: 100
-    
+
 - type: latheRecipe
   id: WeaponLaserCarbine
   result: WeaponLaserCarbine
@@ -89,6 +89,15 @@
     Plastic: 250
     Gold: 100
 
+- type: latheRecipe
+  id: ClothingBackpackElectropack
+  result: ClothingBackpackElectropack
+  completetime: 4
+  materials:
+    Steel: 500
+    Plastic: 250
+    Cloth: 500
+
 - type: latheRecipe
   id: ForensicPad
   result: ForensicPad
@@ -655,7 +664,7 @@
     Steel: 1000
     Glass: 500
     Plastic: 500
-     
+
 - type: latheRecipe
   id: MagazineGrenadeEmpty
   result: MagazineGrenadeEmpty
@@ -663,7 +672,7 @@
   materials:
      Steel: 150
      Plastic: 50
-     
+
 - type: latheRecipe
   id: GrenadeEMP
   result: GrenadeEMP
@@ -672,7 +681,7 @@
      Steel: 150
      Plastic: 100
      Glass: 20
-     
+
 - type: latheRecipe
   id: GrenadeBlast
   result: GrenadeBlast
@@ -681,7 +690,7 @@
      Steel: 450
      Plastic: 300
      Gold: 150
-     
+
 - type: latheRecipe
   id: GrenadeFlash
   result: GrenadeFlash
diff --git a/Resources/Prototypes/Research/arsenal.yml b/Resources/Prototypes/Research/arsenal.yml
index 1cfa1fec80..553258fdb3 100644
--- a/Resources/Prototypes/Research/arsenal.yml
+++ b/Resources/Prototypes/Research/arsenal.yml
@@ -58,8 +58,8 @@
   cost: 5000
   recipeUnlocks:
   - MagazineShotgunBeanbag
-  - BoxShellTranquilizer 
-  - BoxBeanbag 
+  - BoxShellTranquilizer
+  - BoxBeanbag
   - WeaponDisabler
 
 - type: technology
@@ -115,6 +115,18 @@
   - ExplosivePayload
   - ChemicalPayload
 
+- type: technology
+  id: SpecialMeans
+  name: research-technology-special-means
+  icon:
+    sprite: Clothing/Back/Backpacks/electropack.rsi
+    state: icon
+  discipline: Arsenal
+  tier: 1
+  cost: 5000
+  recipeUnlocks:
+  - ClothingBackpackElectropack
+
 # Tier 2
 
 - type: technology
@@ -144,7 +156,7 @@
 - type: technology
   id: BasicShuttleArmament
   name: research-technology-basic-shuttle-armament
-  icon: 
+  icon:
     sprite: Structures/Power/cage_recharger.rsi
     state: full
   discipline: Arsenal
@@ -189,11 +201,11 @@
   cost: 15000
   recipeUnlocks:
   - WeaponLaserSvalinn
-  
+
 - type: technology
   id: AdvancedShuttleWeapon
   name: research-technology-advanced-shuttle-weapon
-  icon: 
+  icon:
     sprite: Objects/Weapons/Guns/Ammunition/Magazine/Grenade/grenade_cartridge.rsi
     state: icon
   discipline: Arsenal
diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/equipped-BACKPACK.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/equipped-BACKPACK.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ea6cdc4b05c396229eb4d75f858c4747ac93525
GIT binary patch
literal 594
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9
z?C1WI$O`0#1o(uwCM6|hWMr6`nc3Od$;im$<m4=3V$#>w_w@9fIB{Zlc(}8(vzC^Y
zzrX+V>C;V3)n?8pb#TzHu8MA%oui_nvTN5aFE6i(iV8nJzo@7v6%{F<!B5ucmI5i6
zk|4j}|F{8z_@+nKf$BI5JR*x382Ao@Fyrz36)8Z6yL!4fhD5l(y_#6utiZ$eAd4f+
z$G!Bf*XjTN=W|cAnsdo=yZ-LSZ`nQcZCKZp%ly#~ZeVKpqxoUR^9!rKF+6y!b#2m;
zp9>zjtmPI}P<SP!D0=B$nn;N9VI%h=VcR>7M|h_-a5foPGHl3=T%mYvlE{k_8vg>s
zU$1A(_{kSFm)-B)9*O!>OdQvu9AvXkzTr6XJAB?J_YKeMHvE(|S<++vO7g<~Z(^Sr
z)_>P|!r-*}{4J%UQInW9*>M=@IH$BHZkWPaQ~dJYF7G_%Kkf?qT&nu7&XC-o&Z^<y
z!N?^L)FAR8y<h%6?@V()hS-BUd0Dm>+%K(?wqq<139DE3;I@oly48C>m*L0tIg=+E
zGx~8en6KQlvvUK}ht;-BLAIVOZxlkAjwruj&)L)L8_D?2p7mX!Rjkt4`G22#&Qedv
z=L%?;P|siSpV6Rq-j072Qi8xx=1Yz6O!M_+&;qhKfEWZW1y2T1p00i_>zopr0MD@O
AUH||9

literal 0
HcmV?d00001

diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/icon.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4b755e0082d7d553160ab8724fc32aefea508b8
GIT binary patch
literal 457
zcmeAS@N?(olHy`uVBq!ia0vp^2|%pC!3-pC$@QiIDaPU;cPEB*=VV?2Ic5PqA+Ba-
zX12Dr6%`e&t*x%EuF=uaE0vWOF)^8%s;Q_*&74tMT@~%%pucL@tbk<Cmf1PdijqLB
z`|{6+04br8Aiv=M=z!tT{{2EgyOcd$978-h-%h*P*KEM!np-Zj$zaOA|L6OJy*4LA
zoeYY%+N+>?sjx=&p~iu?XPed^+;7Anq3*n*)4W*aj>`(Q4Ii@_3L3qq_1NF4lCGCf
zU}o%`(^IqN;;HE7pvKdpFF1YnNHG+wJJ4z0FzLXF-?AGrs)80ahBrToQJJd2*|W#A
zA;^*Mvha+G{mlAYM!P0+@$Q+oFUGOQaA{1VH2><E_tkf{-#orZWQN)+;~SO94tD1v
zPyXhd!eX{h`h(bq-7&SF9u|bj*VG>5fBl~A#0=&?Rk11!ob4>u@o}F@AM8D-6RQxt
zWHSp}9Pf;8{ZBflvbgQ5=XRN<H)GD8ORPP~i)UM!2yQ*g_xkOgy%!J7IVJ3urz8CS
omSs-R^yr`Nf49}}mi%Qrls=E?#GbWsz`$hiboFyt=akR{0BxkgEdT%j

literal 0
HcmV?d00001

diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-left.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-left.png
new file mode 100644
index 0000000000000000000000000000000000000000..49f6243a2c17e84e1a10b2738e6741a6bc60af74
GIT binary patch
literal 432
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9
z?C1WI$O`1E1o(uw#>K_i+1cgf<Sb%hnmBQykEf@psoKmLrPWo@4i5TST3SGblb=T}
z0#ZCBL4Lvi5r9GH{<(8Nan1sd$YKTtzQZ8Qcszea3Q+K|r;B4qg!|iR-h9mlJggfF
zf2e%lpP$^aD#2bQ#w@lXY=RZ@*|wRtyn`k%sj_MY{0n6WXI|WU;=cnU<HtA0I@PW8
z^%WfN>*p+6*>Rw~(KSghd%>YEF%Fw_F7+oioDvZ*SbO`=Gv0`AcR88z10tW*v3=cR
zyP~y4o?9zl`ugexM)qPaew6NJn#Q+4!HFwq!4d^lFRnU)8@!2)zU(SHJUf}E^JQGy
zcCf*pUExFNN7g0G0yoSg7+x~4Y~y8fuwrP;ZDyRn_k-s{`;PiWwN?xEC)`S404lnZ
z7an89;Mg<0>6%?f^vc`TB^m#K-sMY;@J#ddWdH>i2M~k6rQpdR%G1@)Wt~$(69D&y
Bn{xmF

literal 0
HcmV?d00001

diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-right.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-right.png
new file mode 100644
index 0000000000000000000000000000000000000000..26901ce4c9aa65f2f2b4376e79e529f822edfb4a
GIT binary patch
literal 441
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9
z?C1WI$O`1E1o(uw#>K_i+1V{(V#>+MnK*HxkEf@psakba^voHh4i5TST3SGbiRXD%
z0x6!7Aiv=M2*4n8|J*sCIA?)JWHAE+-(e7DJf6QI1t|E&)5S3)!u{>E(?ZP#9IlE9
zYft|FpTAT{_1Z~i^Vov0>y2|d^Y%=Td&DHpp;yrOA-sw4`cX}f4k_z~<Z|(Z1$oSe
zHqK`CxRcNDO!NIJhjYf?r5Yw~QskZxumA2Z&uWHqOkAI(=ig~Kw5ey$p`UDpf7u!}
z^Za!iB-HPoZSwf+voYG?_c|_@i`6wp-@I$uz2g9D6L*ZljtAieIy!tkQ(Jn20`xhT
zg~^CCxp_KVO4tabCTw(bR$MWiMKarDiusCZAbJkV)=3J<y=>YUQ$SpirYexCX3?7e
zzqd{*Fx6o-uh8&#$$ZXe<*OH~qyvEd=1Yz6O!M_+0EHU|5QD&_;K?A$)78&qol`;+
E0L)3E*8l(j

literal 0
HcmV?d00001

diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/meta.json b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/meta.json
new file mode 100644
index 0000000000..4e7738117e
--- /dev/null
+++ b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/meta.json
@@ -0,0 +1,33 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/2d26ce62c273d025bed77a0e6c4bdc770b789bb0",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "icon",
+      "delays": [
+        [
+          0.1,
+          0.1,
+          0.1
+        ]
+      ]
+    },
+    {
+      "name": "equipped-BACKPACK",
+      "directions": 4
+    },
+    {
+      "name": "inhand-left",
+      "directions": 4
+    },
+    {
+      "name": "inhand-right",
+      "directions": 4
+    }
+  ]
+}
diff --git a/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/equipped-NECK.png b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/equipped-NECK.png
new file mode 100644
index 0000000000000000000000000000000000000000..ffca3249f13d35ed449938c85fc67b5c0e740977
GIT binary patch
literal 465
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%z3q4&NLn`LH
zy=9o!Y#_q+z&=!8U}f>7IE~{c9L(|#xQUx7RlYeSI6=TYA>q`4mg@^R+WKVgEHvDD
zY1Jdig*yM$T{$Fw+Ugkn<yTNrQku0uqsTbDR&VL0!usgbYhSMO|Ef|M@%meM;`ddh
z+H3X0M0Re}e-&5D|3C6m!R0xO43Y_3@)m|<ZOJ=cy?dWX`Tt$(7#I#09Ig7xbpO{P
zeFaay)ypgwvj<GGEZZF}^UeLs_GgoxY!NDd5Lhp<?!~UtKcw4Mm)E{;J8U3fzjJB)
zoDL-Lf^k~kf#)Ay`ik@&D@^&<bwd87-Ms$C9#<y6V8{{5Ui|Sl8^eCZ)|LNOuPn7*
zc1-9|#h%xC8w9t?cAQ`czAie&>+B?BCg0+=pp`3Mb4eyNykE#G-Cus!bxue(n@Yoc
z4I3WjWdGlvw}nl!G@HG)#Czi5e;wPubJ#ZUr)xZVC|E1wer&E}gqH-H+b?@{VAz1c
uqKo2e_qlSq7pxNo#spt#glC$sFM}44%>l$9a4C2)i1Kvxb6Mw<&;$VGYr@(9

literal 0
HcmV?d00001

diff --git a/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/icon.png b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8e0a9cb8e4fab549dd7fe5de64098076d68774a
GIT binary patch
literal 800
zcmV+*1K<3KP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800001b5ch_0Itp)
z=>Px%z)3_wR9J=Wls{<OKorJ*DeagcP@FAwhGI$<%aFmVhq#bT#bAm9g#<6orVe!{
z)6GNC7DH>u6bN2Al~8x^Rx*@e4^4vUpo=M?7D666cDN4F2`Tzxr=ifn-y+?+cklb&
zy>}1hm}8E=fUehM()yp1ce}?@k=|V27WZZ$^#2!&TL@tWe1tG57PmsacNU-sb-gAf
zNJR#a$*iI4HJQn*;kQeW{&Uj=+@D6S0-w30lZoy5q*BY?dTHq)J4TC_dd(A5EN%fv
z!)PtU@k}iNz>d-4%e#G#Ow-mwU$v)hWL889(Dj-ejV=+w^aA@c7sAA{#(}TGWMX?a
zC4|Y2(Ml{uNPw=_WTmp}1=F;3J~rQwt2eN$F_txEI6MOgd~@{%2k!N&bCZe96;P?{
z#)^pT0e`lB3<mv3zbEe3vc{xR%LrkTt2cOG+T?XY{D6ROAP)fB9s`i8H~8dc)hy(B
zZnQ8UvY~oF+i1z7LY{0^!?MQS^{RMN1zZ86(Io(m<8bNAxYs*b=p|$VP=y70C*VF=
z^cs$PUd6qMKo9s`%H?g23i)f|-ieoWTrK@}b!IT=Q!a0NKA)AaB(y=6N}ICVJ(i`?
zW@zT0o}PxrZk*j8rwuYnK<E`zeKoFD_h`C@)a!_=r)`e|-QEOQt?qgIkARJc{)v1A
zj^iMNNjkmCiW|%#*i2ESQ2R}gE3Qol6UT8PMaK$oYc9*>Z5DwQU@5akzyB48o_7Ad
zMIU*mbC}q`v2#F)c;>Djr&k#c&(wSmA)Q{O+x>{9t@GWreQ?R+I1&XY5jnR99{2`Q
zyS<O&IBaY@<<*M@=NItf_c3stwFoAWh^dqCt=l+-&Y{d^wa{cdzX1Cuh+o(CPUnzb
z%&>~W%#HwsLSFjc^-XkUxbiy*P$J^?Zc9GL9RCIW0O(JZ+iLn{RsaA14rN$LW=%~1
eDgXcg2mk;800000(o>TF0000<MNUMnLSTY4GkC24

literal 0
HcmV?d00001

diff --git a/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/meta.json b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/meta.json
new file mode 100644
index 0000000000..3119a51a15
--- /dev/null
+++ b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/meta.json
@@ -0,0 +1,19 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Drawn by EmoGarbage404 (github) for Space Station 14",
+    "size": {
+      "x": 32,
+      "y": 32
+    },
+
+    "states": [
+      {
+        "name": "equipped-NECK",
+        "directions": 4
+      },
+      {
+        "name": "icon"
+      }
+    ]
+  }

From 25074d0719362b87eef7de9561ff7b036496959a Mon Sep 17 00:00:00 2001
From: PJBot <pieterjan.briers+bot@gmail.com>
Date: Thu, 15 Aug 2024 14:31:47 +0000
Subject: [PATCH 2/5] Automatic changelog update

---
 Resources/Changelog/Changelog.yml | 24 +++++++++++++++++-------
 1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index 006ed4f5d0..403488642e 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -1,11 +1,4 @@
 Entries:
-- author: ElectroJr
-  changes:
-  - message: Ghosts can once again open paper & other UIs
-    type: Fix
-  id: 6614
-  time: '2024-05-24T05:03:03.0000000+00:00'
-  url: https://github.com/space-wizards/space-station-14/pull/27999
 - author: nikthechampiongr
   changes:
   - message: Firelocks will no longer randomly pulse closing lights.
@@ -3811,3 +3804,20 @@
   id: 7113
   time: '2024-08-15T12:34:41.0000000+00:00'
   url: https://github.com/space-wizards/space-station-14/pull/30865
+- author: to4no_fix
+  changes:
+  - message: Added a new electropack that shocks when a trigger is triggered
+    type: Add
+  - message: Added a new shock collar that shocks when a trigger is triggered
+    type: Add
+  - message: Two shock collars and two remote signallers added to the warden's locker
+    type: Add
+  - message: Shock collar added as a new target for the thief
+    type: Add
+  - message: A new Special Means technology has been added to the Arsenal research
+      branch at the 1st research level. Its research opens up the possibility of producing
+      electropacks at security techfab. The cost of technology research is 5000
+    type: Add
+  id: 7114
+  time: '2024-08-15T14:30:39.0000000+00:00'
+  url: https://github.com/space-wizards/space-station-14/pull/30529

From 2e3365793c314702a5f49db0a1451ed4e1e6033f Mon Sep 17 00:00:00 2001
From: Mervill <mervills.email@gmail.com>
Date: Thu, 15 Aug 2024 07:45:13 -0700
Subject: [PATCH 3/5] Greatly improve the usability of the Gas Analyzer.
 (#30763)

* greatly improve how the gas analyzer behaves

* don't close the analyzer when the object goes out of range

* cleanup

* always switch to the device tab when a new device is analyzed

* modern api part one

* modern api part 2

* modern api part three

* file scope namespace
---
 .../Atmos/UI/GasAnalyzerWindow.xaml.cs        |  10 +
 .../Atmos/EntitySystems/GasAnalyzerSystem.cs  | 411 +++++++++---------
 .../EntitySystems/RadiationCollectorSystem.cs |   2 +-
 .../Atmos/Components/GasAnalyzerComponent.cs  |   3 -
 .../en-US/atmos/gas-analyzer-component.ftl    |   2 +-
 5 files changed, 213 insertions(+), 215 deletions(-)

diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
index b54af3a587..bb24da44e1 100644
--- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
+++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
@@ -16,6 +16,8 @@ namespace Content.Client.Atmos.UI
     [GenerateTypedNameReferences]
     public sealed partial class GasAnalyzerWindow : DefaultWindow
     {
+        private NetEntity _currentEntity = NetEntity.Invalid;
+
         public GasAnalyzerWindow()
         {
             RobustXamlLoader.Load(this);
@@ -55,6 +57,13 @@ public void Populate(GasAnalyzerUserMessage msg)
             // Device Tab
             if (msg.NodeGasMixes.Length > 1)
             {
+                if (_currentEntity != msg.DeviceUid)
+                {
+                    // when we get new device data switch to the device tab
+                    CTabContainer.CurrentTab = 0;
+                    _currentEntity = msg.DeviceUid;
+                }
+
                 CTabContainer.SetTabVisible(0, true);
                 CTabContainer.SetTabTitle(0, Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.DeviceName)));
                 // Set up Grid
@@ -143,6 +152,7 @@ public void Populate(GasAnalyzerUserMessage msg)
                 CTabContainer.SetTabVisible(0, false);
                 CTabContainer.CurrentTab = 1;
                 minSize = new Vector2(CEnvironmentMix.DesiredSize.X + 40, MinSize.Y);
+                _currentEntity = NetEntity.Invalid;
             }
 
             MinSize = minSize;
diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
index 0f4490cd7e..81f0b96d02 100644
--- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
@@ -1,5 +1,4 @@
 using System.Linq;
-using Content.Server.Atmos;
 using Content.Server.Atmos.Components;
 using Content.Server.NodeContainer;
 using Content.Server.NodeContainer.Nodes;
@@ -10,274 +9,266 @@
 using Content.Shared.Interaction.Events;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
-using Robust.Shared.Player;
 using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
 
-namespace Content.Server.Atmos.EntitySystems
+namespace Content.Server.Atmos.EntitySystems;
+
+[UsedImplicitly]
+public sealed class GasAnalyzerSystem : EntitySystem
 {
-    [UsedImplicitly]
-    public sealed class GasAnalyzerSystem : EntitySystem
+    [Dependency] private readonly PopupSystem _popup = default!;
+    [Dependency] private readonly AtmosphereSystem _atmo = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
+    [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+
+    /// <summary>
+    /// Minimum moles of a gas to be sent to the client.
+    /// </summary>
+    private const float UIMinMoles = 0.01f;
+
+    public override void Initialize()
     {
-        [Dependency] private readonly PopupSystem _popup = default!;
-        [Dependency] private readonly AtmosphereSystem _atmo = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-        [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
-        [Dependency] private readonly TransformSystem _transform = default!;
-
-        /// <summary>
-        /// Minimum moles of a gas to be sent to the client.
-        /// </summary>
-        private const float UIMinMoles = 0.01f;
-
-        public override void Initialize()
-        {
-            base.Initialize();
+        base.Initialize();
 
-            SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
-            SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
-            SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
-            SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
-        }
+        SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
+        SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
+        SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
+    }
 
-        public override void Update(float frameTime)
+    public override void Update(float frameTime)
+    {
+        var query = EntityQueryEnumerator<ActiveGasAnalyzerComponent>();
+        while (query.MoveNext(out var uid, out var analyzer))
         {
-            var query = EntityQueryEnumerator<ActiveGasAnalyzerComponent>();
-            while (query.MoveNext(out var uid, out var analyzer))
-            {
-                // Don't update every tick
-                analyzer.AccumulatedFrametime += frameTime;
+            // Don't update every tick
+            analyzer.AccumulatedFrametime += frameTime;
 
-                if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval)
-                    continue;
+            if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval)
+                continue;
 
-                analyzer.AccumulatedFrametime -= analyzer.UpdateInterval;
+            analyzer.AccumulatedFrametime -= analyzer.UpdateInterval;
 
-                if (!UpdateAnalyzer(uid))
-                    RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
-            }
+            if (!UpdateAnalyzer(uid))
+                RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
         }
+    }
 
-        /// <summary>
-        /// Activates the analyzer when used in the world, scanning either the target entity or the tile clicked
-        /// </summary>
-        private void OnAfterInteract(EntityUid uid, GasAnalyzerComponent component, AfterInteractEvent args)
+    /// <summary>
+    /// Activates the analyzer when used in the world, scanning the target entity (if it exists) and the tile the analyzer is in
+    /// </summary>
+    private void OnAfterInteract(Entity<GasAnalyzerComponent> entity, ref AfterInteractEvent args)
+    {
+        var target = args.Target;
+        if (target != null && !_interactionSystem.InRangeUnobstructed((args.User, null), (target.Value, null)))
         {
-            if (!args.CanReach)
-            {
-                _popup.PopupEntity(Loc.GetString("gas-analyzer-component-player-cannot-reach-message"), args.User, args.User);
-                return;
-            }
-            ActivateAnalyzer(uid, component, args.User, args.Target);
-            args.Handled = true;
+            target = null; // if the target is out of reach, invalidate it
         }
+        // always run the analyzer, regardless of weather or not there is a target
+        // since we can always show the local environment.
+        ActivateAnalyzer(entity, args.User, target);
+        args.Handled = true;
+    }
 
-        /// <summary>
-        /// Activates the analyzer with no target, so it only scans the tile the user was on when activated
-        /// </summary>
-        private void OnUseInHand(EntityUid uid, GasAnalyzerComponent component, UseInHandEvent args)
+    /// <summary>
+    /// Activates the analyzer with no target, so it only scans the tile the user was on when activated
+    /// </summary>
+    private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
+    {
+        if (!entity.Comp.Enabled)
         {
-            ActivateAnalyzer(uid, component, args.User);
-            args.Handled = true;
+            ActivateAnalyzer(entity, args.User);
         }
-
-        /// <summary>
-        /// Handles analyzer activation logic
-        /// </summary>
-        private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, EntityUid user, EntityUid? target = null)
+        else
         {
-            if (!TryOpenUserInterface(uid, user, component))
-                return;
-
-            component.Target = target;
-            component.User = user;
-            if (target != null)
-                component.LastPosition = Transform(target.Value).Coordinates;
-            else
-                component.LastPosition = null;
-            component.Enabled = true;
-            Dirty(uid, component);
-            UpdateAppearance(uid, component);
-            EnsureComp<ActiveGasAnalyzerComponent>(uid);
-            UpdateAnalyzer(uid, component);
+            DisableAnalyzer(entity, args.User);
         }
+        args.Handled = true;
+    }
 
-        /// <summary>
-        /// Close the UI, turn the analyzer off, and don't update when it's dropped
-        /// </summary>
-        private void OnDropped(EntityUid uid, GasAnalyzerComponent component, DroppedEvent args)
-        {
-            if (args.User is var userId && component.Enabled)
-                _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
-            DisableAnalyzer(uid, component, args.User);
-        }
+    /// <summary>
+    /// Handles analyzer activation logic
+    /// </summary>
+    private void ActivateAnalyzer(Entity<GasAnalyzerComponent> entity, EntityUid user, EntityUid? target = null)
+    {
+        if (!_userInterface.TryOpenUi(entity.Owner, GasAnalyzerUiKey.Key, user))
+            return;
+
+        entity.Comp.Target = target;
+        entity.Comp.User = user;
+        entity.Comp.Enabled = true;
+        Dirty(entity);
+        _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
+        EnsureComp<ActiveGasAnalyzerComponent>(entity.Owner);
+        UpdateAnalyzer(entity.Owner, entity.Comp);
+    }
 
-        /// <summary>
-        /// Closes the UI, sets the icon to off, and removes it from the update list
-        /// </summary>
-        private void DisableAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null, EntityUid? user = null)
-        {
-            if (!Resolve(uid, ref component))
-                return;
+    /// <summary>
+    /// Close the UI, turn the analyzer off, and don't update when it's dropped
+    /// </summary>
+    private void OnDropped(Entity<GasAnalyzerComponent> entity, ref DroppedEvent args)
+    {
+        if (args.User is var userId && entity.Comp.Enabled)
+            _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
+        DisableAnalyzer(entity, args.User);
+    }
+
+    /// <summary>
+    /// Closes the UI, sets the icon to off, and removes it from the update list
+    /// </summary>
+    private void DisableAnalyzer(Entity<GasAnalyzerComponent> entity, EntityUid? user = null)
+    {
+        _userInterface.CloseUi(entity.Owner, GasAnalyzerUiKey.Key, user);
 
-            _userInterface.CloseUi(uid, GasAnalyzerUiKey.Key, user);
+        entity.Comp.Enabled = false;
+        Dirty(entity);
+        _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
+        RemCompDeferred<ActiveGasAnalyzerComponent>(entity.Owner);
+    }
 
-            component.Enabled = false;
-            Dirty(uid, component);
-            UpdateAppearance(uid, component);
-            RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
-        }
+    /// <summary>
+    /// Disables the analyzer when the user closes the UI
+    /// </summary>
+    private void OnDisabledMessage(Entity<GasAnalyzerComponent> entity, ref GasAnalyzerDisableMessage message)
+    {
+        DisableAnalyzer(entity);
+    }
 
-        /// <summary>
-        /// Disables the analyzer when the user closes the UI
-        /// </summary>
-        private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message)
-        {
-            DisableAnalyzer(uid, component);
-        }
+    /// <summary>
+    /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
+    /// </summary>
+    private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
 
-        private bool TryOpenUserInterface(EntityUid uid, EntityUid user, GasAnalyzerComponent? component = null)
+        // check if the user has walked away from what they scanned
+        if (component.Target.HasValue)
         {
-            if (!Resolve(uid, ref component, false))
-                return false;
+            // Listen! Even if you don't want the Gas Analyzer to work on moving targets, you should use
+            // this code to determine if the object is still generally in range so that the check is consistent with the code
+            // in OnAfterInteract() and also consistent with interaction code in general.
+            if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null)))
+            {
+                if (component.User is { } userId && component.Enabled)
+                    _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), userId, userId);
 
-            return _userInterface.TryOpenUi(uid, GasAnalyzerUiKey.Key, user);
+                component.Target = null;
+            }
         }
 
-        /// <summary>
-        /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
-        /// </summary>
-        private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null)
+        var gasMixList = new List<GasMixEntry>();
+
+        // Fetch the environmental atmosphere around the scanner. This must be the first entry
+        var tileMixture = _atmo.GetContainingMixture(uid, true);
+        if (tileMixture != null)
         {
-            if (!Resolve(uid, ref component))
-                return false;
+            gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
+                GenerateGasEntryArray(tileMixture)));
+        }
+        else
+        {
+            // No gases were found
+            gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
+        }
 
-            if (!TryComp(component.User, out TransformComponent? xform))
+        var deviceFlipped = false;
+        if (component.Target != null)
+        {
+            if (Deleted(component.Target))
             {
-                DisableAnalyzer(uid, component);
+                component.Target = null;
+                DisableAnalyzer((uid, component), component.User);
                 return false;
             }
 
-            // check if the user has walked away from what they scanned
-            var userPos = xform.Coordinates;
-            if (component.LastPosition.HasValue)
-            {
-                // Check if position is out of range => don't update and disable
-                if (!_transform.InRange(component.LastPosition.Value, userPos, SharedInteractionSystem.InteractionRange))
-                {
-                    if (component.User is { } userId && component.Enabled)
-                        _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
-                    DisableAnalyzer(uid, component, component.User);
-                    return false;
-                }
-            }
-
-            var gasMixList = new List<GasMixEntry>();
+            var validTarget = false;
 
-            // Fetch the environmental atmosphere around the scanner. This must be the first entry
-            var tileMixture = _atmo.GetContainingMixture(uid, true);
-            if (tileMixture != null)
-            {
-                gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
-                    GenerateGasEntryArray(tileMixture)));
-            }
-            else
-            {
-                // No gases were found
-                gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
-            }
+            // gas analyzed was used on an entity, try to request gas data via event for override
+            var ev = new GasAnalyzerScanEvent();
+            RaiseLocalEvent(component.Target.Value, ev);
 
-            var deviceFlipped = false;
-            if (component.Target != null)
+            if (ev.GasMixtures != null)
             {
-                if (Deleted(component.Target))
-                {
-                    component.Target = null;
-                    DisableAnalyzer(uid, component, component.User);
-                    return false;
-                }
-
-                // gas analyzed was used on an entity, try to request gas data via event for override
-                var ev = new GasAnalyzerScanEvent();
-                RaiseLocalEvent(component.Target.Value, ev);
-
-                if (ev.GasMixtures != null)
+                foreach (var mixes in ev.GasMixtures)
                 {
-                    foreach (var mixes in ev.GasMixtures)
+                    if (mixes.Item2 != null)
                     {
-                        if (mixes.Item2 != null)
-                            gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
+                        gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
+                        validTarget = true;
                     }
-
-                    deviceFlipped = ev.DeviceFlipped;
                 }
-                else
+
+                deviceFlipped = ev.DeviceFlipped;
+            }
+            else
+            {
+                // No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent
+                if (TryComp(component.Target, out NodeContainerComponent? node))
                 {
-                    // No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent
-                    if (TryComp(component.Target, out NodeContainerComponent? node))
+                    foreach (var pair in node.Nodes)
                     {
-                        foreach (var pair in node.Nodes)
+                        if (pair.Value is PipeNode pipeNode)
                         {
-                            if (pair.Value is PipeNode pipeNode)
-                            {
-                                // check if the volume is zero for some reason so we don't divide by zero
-                                if (pipeNode.Air.Volume == 0f)
-                                    continue;
-                                // only display the gas in the analyzed pipe element, not the whole system
-                                var pipeAir = pipeNode.Air.Clone();
-                                pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
-                                pipeAir.Volume = pipeNode.Volume;
-                                gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
-                            }
+                            // check if the volume is zero for some reason so we don't divide by zero
+                            if (pipeNode.Air.Volume == 0f)
+                                continue;
+                            // only display the gas in the analyzed pipe element, not the whole system
+                            var pipeAir = pipeNode.Air.Clone();
+                            pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
+                            pipeAir.Volume = pipeNode.Volume;
+                            gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
+                            validTarget = true;
                         }
                     }
                 }
             }
 
-            // Don't bother sending a UI message with no content, and stop updating I guess?
-            if (gasMixList.Count == 0)
-                return false;
-
-            _userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key,
-                new GasAnalyzerUserMessage(gasMixList.ToArray(),
-                    component.Target != null ? Name(component.Target.Value) : string.Empty,
-                    GetNetEntity(component.Target) ?? NetEntity.Invalid,
-                    deviceFlipped));
-            return true;
+            // If the target doesn't actually have any gas mixes to add,
+            // invalidate it as the target
+            if (!validTarget)
+            {
+                component.Target = null;
+            }
         }
 
-        /// <summary>
-        /// Sets the appearance based on the analyzers Enabled state
-        /// </summary>
-        private void UpdateAppearance(EntityUid uid, GasAnalyzerComponent analyzer)
-        {
-            _appearance.SetData(uid, GasAnalyzerVisuals.Enabled, analyzer.Enabled);
-        }
+        // Don't bother sending a UI message with no content, and stop updating I guess?
+        if (gasMixList.Count == 0)
+            return false;
 
-        /// <summary>
-        /// Generates a GasEntry array for a given GasMixture
-        /// </summary>
-        private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
-        {
-            var gases = new List<GasEntry>();
+        _userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key,
+            new GasAnalyzerUserMessage(gasMixList.ToArray(),
+                component.Target != null ? Name(component.Target.Value) : string.Empty,
+                GetNetEntity(component.Target) ?? NetEntity.Invalid,
+                deviceFlipped));
+        return true;
+    }
 
-            for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
-            {
-                var gas = _atmo.GetGas(i);
+    /// <summary>
+    /// Generates a GasEntry array for a given GasMixture
+    /// </summary>
+    private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
+    {
+        var gases = new List<GasEntry>();
+
+        for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
+        {
+            var gas = _atmo.GetGas(i);
 
-                if (mixture?[i] <= UIMinMoles)
-                    continue;
+            if (mixture?[i] <= UIMinMoles)
+                continue;
 
-                if (mixture != null)
-                {
-                    var gasName = Loc.GetString(gas.Name);
-                    gases.Add(new GasEntry(gasName, mixture[i], gas.Color));
-                }
+            if (mixture != null)
+            {
+                var gasName = Loc.GetString(gas.Name);
+                gases.Add(new GasEntry(gasName, mixture[i], gas.Color));
             }
+        }
 
-            var gasesOrdered = gases.OrderByDescending(gas => gas.Amount);
+        var gasesOrdered = gases.OrderByDescending(gas => gas.Amount);
 
-            return gasesOrdered.ToArray();
-        }
+        return gasesOrdered.ToArray();
     }
 }
 
diff --git a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs
index c262988c86..3fab18b1b7 100644
--- a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs
@@ -1,6 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using Content.Server.Atmos;
+using Content.Server.Atmos.EntitySystems;
 using Content.Server.Atmos.Components;
 using Content.Server.Popups;
 using Content.Server.Power.Components;
diff --git a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs
index dec9516c01..c143e8cf85 100644
--- a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs
+++ b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs
@@ -13,9 +13,6 @@ public sealed partial class GasAnalyzerComponent : Component
     [ViewVariables]
     public EntityUid User;
 
-    [ViewVariables(VVAccess.ReadWrite)]
-    public EntityCoordinates? LastPosition;
-
     [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)]
     public bool Enabled;
 
diff --git a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl
index 652bb19cb5..a2cb5301b2 100644
--- a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl
+++ b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl
@@ -1,6 +1,6 @@
 ## Entity
 
-gas-analyzer-component-player-cannot-reach-message = You can't reach there.
+gas-analyzer-object-out-of-range = The object went out of range.
 gas-analyzer-shutoff = The gas analyzer shuts off.
 
 ## UI

From 5da2b320991dcce79d386dea3f613fbc5e64795e Mon Sep 17 00:00:00 2001
From: PJBot <pieterjan.briers+bot@gmail.com>
Date: Thu, 15 Aug 2024 14:46:20 +0000
Subject: [PATCH 4/5] Automatic changelog update

---
 Resources/Changelog/Changelog.yml | 23 ++++++++++++++++-------
 1 file changed, 16 insertions(+), 7 deletions(-)

diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index 403488642e..ee4f9cd5a6 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -1,11 +1,4 @@
 Entries:
-- author: nikthechampiongr
-  changes:
-  - message: Firelocks will no longer randomly pulse closing lights.
-    type: Fix
-  id: 6615
-  time: '2024-05-24T14:44:42.0000000+00:00'
-  url: https://github.com/space-wizards/space-station-14/pull/28227
 - author: ElectroJr
   changes:
   - message: Fixed modular grenade visuals getting stuck in an incorrect state.
@@ -3821,3 +3814,19 @@
   id: 7114
   time: '2024-08-15T14:30:39.0000000+00:00'
   url: https://github.com/space-wizards/space-station-14/pull/30529
+- author: Mervill
+  changes:
+  - message: The Gas Analyzer won't spuriously shut down for seemly no reason.
+    type: Tweak
+  - message: The Gas Analyzer will always switch to the device tab when a new object
+      is scanned.
+    type: Tweak
+  - message: The Gas Analyzer's interaction range is now equal to the standard interaction
+      range
+    type: Fix
+  - message: Clicking the Gas Analyzer when it's in your hand has proper enable/disable
+      behavior.
+    type: Fix
+  id: 7115
+  time: '2024-08-15T14:45:13.0000000+00:00'
+  url: https://github.com/space-wizards/space-station-14/pull/30763

From 84f9dd0f0b8ef46f07503277827669a10fc63819 Mon Sep 17 00:00:00 2001
From: IgorAnt028 <118114530+IgorAnt028@users.noreply.github.com>
Date: Thu, 15 Aug 2024 18:00:56 +0300
Subject: [PATCH 5/5] Fix false and true in player-panel.ftl (#31043)

Fix false and true in player-panel

Add a new True string
---
 .../Administration/UI/PlayerPanel/PlayerPanel.xaml.cs           | 2 +-
 Resources/Locale/en-US/administration/ui/player-panel.ftl       | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
index f20e47b6a1..53cc8faa10 100644
--- a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
+++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
@@ -70,7 +70,7 @@ public void SetWhitelisted(bool? whitelisted)
         else
         {
             Whitelisted.Text = Loc.GetString("player-panel-whitelisted");
-            WhitelistToggle.Text = whitelisted.Value.ToString();
+            WhitelistToggle.Text = whitelisted.Value ? Loc.GetString("player-panel-true") : Loc.GetString("player-panel-false");
             WhitelistToggle.Visible = true;
             _isWhitelisted = whitelisted.Value;
         }
diff --git a/Resources/Locale/en-US/administration/ui/player-panel.ftl b/Resources/Locale/en-US/administration/ui/player-panel.ftl
index ed63dd6d10..cfb014948d 100644
--- a/Resources/Locale/en-US/administration/ui/player-panel.ftl
+++ b/Resources/Locale/en-US/administration/ui/player-panel.ftl
@@ -20,3 +20,4 @@ player-panel-logs = Logs
 player-panel-delete = Delete
 player-panel-rejuvenate = Rejuvenate
 player-panel-false = False
+player-panel-true = True