]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
EasyPry airlocks for arrivals. Now also prying refactor I guess (#19394)
authornikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Thu, 28 Sep 2023 11:34:21 +0000 (11:34 +0000)
committerGitHub <noreply@github.com>
Thu, 28 Sep 2023 11:34:21 +0000 (21:34 +1000)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
24 files changed:
Content.Client/Doors/AirlockSystem.cs
Content.Server/Doors/Systems/AirlockSystem.cs
Content.Server/Doors/Systems/DoorSystem.cs
Content.Server/Doors/Systems/FirelockSystem.cs
Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
Content.Server/NPC/Systems/NPCSteeringSystem.cs
Content.Server/Zombies/ZombieSystem.Transform.cs
Content.Shared/Doors/Components/DoorComponent.cs
Content.Shared/Doors/DoorEvents.cs
Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs
Content.Shared/Doors/Systems/SharedDoorSystem.cs
Content.Shared/Prying/Components/PryUnpoweredComponent.cs [new file with mode: 0644]
Content.Shared/Prying/Components/PryingComponent.cs [new file with mode: 0644]
Content.Shared/Prying/Systems/PryingSystem.cs [new file with mode: 0644]
Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs
Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml
Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml
Resources/Prototypes/Entities/Objects/Tools/cowtools.yml
Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml
Resources/Prototypes/Entities/Objects/Tools/tools.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/easy_pry.yml [new file with mode: 0644]

index 18b9eae5f34999d62d2fdb1720b28ccccd169450..cc68d090394aa8b56787e7417ab77ba1de74e38e 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Client.Wires.Visualizers;
 using Content.Shared.Doors.Components;
 using Content.Shared.Doors.Systems;
+using Content.Shared.Prying.Components;
 using Robust.Client.Animations;
 using Robust.Client.GameObjects;
 
@@ -15,6 +16,13 @@ public sealed class AirlockSystem : SharedAirlockSystem
         base.Initialize();
         SubscribeLocalEvent<AirlockComponent, ComponentStartup>(OnComponentStartup);
         SubscribeLocalEvent<AirlockComponent, AppearanceChangeEvent>(OnAppearanceChange);
+        SubscribeLocalEvent<AirlockComponent, BeforePryEvent>(OnAirlockPryAttempt);
+    }
+
+    private void OnAirlockPryAttempt(EntityUid uid, AirlockComponent component, ref BeforePryEvent args)
+    {
+        // TODO: Temporary until airlocks predicted.
+        args.Cancelled = true;
     }
 
     private void OnComponentStartup(EntityUid uid, AirlockComponent comp, ComponentStartup args)
index bb75fc7d4764a230d300a867df6dce4c3af70f3b..0ea2755ab66a4b694e99448b8e6449daa3f32415 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Server.DeviceLinking.Events;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
-using Content.Shared.Tools.Components;
 using Content.Server.Wires;
 using Content.Shared.Doors;
 using Content.Shared.Doors.Components;
@@ -9,6 +8,7 @@ using Content.Shared.Doors.Systems;
 using Content.Shared.Interaction;
 using Robust.Server.GameObjects;
 using Content.Shared.Wires;
+using Content.Shared.Prying.Components;
 using Robust.Shared.Prototypes;
 
 namespace Content.Server.Doors.Systems;
@@ -31,9 +31,9 @@ public sealed class AirlockSystem : SharedAirlockSystem
         SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
         SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
         SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
-        SubscribeLocalEvent<AirlockComponent, ActivateInWorldEvent>(OnActivate, before: new [] {typeof(DoorSystem)});
-        SubscribeLocalEvent<AirlockComponent, DoorGetPryTimeModifierEvent>(OnGetPryMod);
-        SubscribeLocalEvent<AirlockComponent, BeforeDoorPryEvent>(OnDoorPry);
+        SubscribeLocalEvent<AirlockComponent, ActivateInWorldEvent>(OnActivate, before: new[] { typeof(DoorSystem) });
+        SubscribeLocalEvent<AirlockComponent, GetPryTimeModifierEvent>(OnGetPryMod);
+        SubscribeLocalEvent<AirlockComponent, BeforePryEvent>(OnBeforePry);
 
     }
 
@@ -169,20 +169,18 @@ public sealed class AirlockSystem : SharedAirlockSystem
         }
     }
 
-    private void OnGetPryMod(EntityUid uid, AirlockComponent component, DoorGetPryTimeModifierEvent args)
+    private void OnGetPryMod(EntityUid uid, AirlockComponent component, ref GetPryTimeModifierEvent args)
     {
         if (_power.IsPowered(uid))
             args.PryTimeModifier *= component.PoweredPryModifier;
     }
 
-    private void OnDoorPry(EntityUid uid, AirlockComponent component, BeforeDoorPryEvent args)
+    private void OnBeforePry(EntityUid uid, AirlockComponent component, ref BeforePryEvent args)
     {
-        if (this.IsPowered(uid, EntityManager))
+        if (this.IsPowered(uid, EntityManager) && !args.PryPowered)
         {
-            if (HasComp<ToolForcePoweredComponent>(args.Tool))
-                return;
-            Popup.PopupEntity(Loc.GetString("airlock-component-cannot-pry-is-powered-message"), uid, args.User);
-            args.Cancel();
+            Popup.PopupClient(Loc.GetString("airlock-component-cannot-pry-is-powered-message"), uid, args.User);
+            args.Cancelled = true;
         }
     }
 
index f9918dfb0a6e60011bba5a4e6086c2c8da92dd11..aa4de6b86bb1d3ac575d72d102d486b5fb690119 100644 (file)
@@ -1,15 +1,12 @@
 using Content.Server.Access;
 using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
-using Content.Server.Construction;
 using Content.Shared.Database;
 using Content.Shared.Doors;
 using Content.Shared.Doors.Components;
 using Content.Shared.Doors.Systems;
 using Content.Shared.Emag.Systems;
 using Content.Shared.Interaction;
-using Content.Shared.Tools.Components;
-using Content.Shared.Verbs;
 using Robust.Shared.Audio;
 using Content.Server.Administration.Logs;
 using Content.Server.Power.EntitySystems;
@@ -17,6 +14,8 @@ using Content.Shared.Tools;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Events;
 using Content.Shared.DoAfter;
+using Content.Shared.Prying.Systems;
+using Content.Shared.Prying.Components;
 using Content.Shared.Tools.Systems;
 
 namespace Content.Server.Doors.Systems;
@@ -27,20 +26,19 @@ public sealed class DoorSystem : SharedDoorSystem
     [Dependency] private readonly DoorBoltSystem _bolts = default!;
     [Dependency] private readonly AirtightSystem _airtightSystem = default!;
     [Dependency] private readonly SharedToolSystem _toolSystem = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] private readonly PryingSystem _pryingSystem = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<DoorComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ConstructionSystem) });
 
-        // Mob prying doors
-        SubscribeLocalEvent<DoorComponent, GetVerbsEvent<AlternativeVerb>>(OnDoorAltVerb);
-
-        SubscribeLocalEvent<DoorComponent, DoorPryDoAfterEvent>(OnPryFinished);
+        SubscribeLocalEvent<DoorComponent, BeforePryEvent>(OnBeforeDoorPry);
         SubscribeLocalEvent<DoorComponent, WeldableAttemptEvent>(OnWeldAttempt);
         SubscribeLocalEvent<DoorComponent, WeldableChangedEvent>(OnWeldChanged);
         SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
+        SubscribeLocalEvent<DoorComponent, PriedEvent>(OnAfterPry);
     }
 
     protected override void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
@@ -49,7 +47,9 @@ public sealed class DoorSystem : SharedDoorSystem
         if (args.Handled || !door.ClickOpen)
             return;
 
-        TryToggleDoor(uid, door, args.User);
+        if (!TryToggleDoor(uid, door, args.User))
+            _pryingSystem.TryPry(uid, args.User, out _);
+
         args.Handled = true;
     }
 
@@ -108,24 +108,7 @@ public sealed class DoorSystem : SharedDoorSystem
             Audio.PlayPvs(soundSpecifier, uid, audioParams);
     }
 
-#region DoAfters
-    /// <summary>
-    ///     Weld or pry open a door.
-    /// </summary>
-    private void OnInteractUsing(EntityUid uid, DoorComponent door, InteractUsingEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        if (!TryComp(args.Used, out ToolComponent? tool))
-            return;
-
-        if (tool.Qualities.Contains(door.PryingQuality))
-        {
-            args.Handled = TryPryDoor(uid, args.Used, args.User, door, out _);
-        }
-    }
-
+    #region DoAfters
     private void OnWeldAttempt(EntityUid uid, DoorComponent component, WeldableAttemptEvent args)
     {
         if (component.CurrentlyCrushing.Count > 0)
@@ -147,69 +130,12 @@ public sealed class DoorSystem : SharedDoorSystem
             SetState(uid, DoorState.Closed, component);
     }
 
-    private void OnDoorAltVerb(EntityUid uid, DoorComponent component, GetVerbsEvent<AlternativeVerb> args)
+    private void OnBeforeDoorPry(EntityUid id, DoorComponent door, ref BeforePryEvent args)
     {
-        if (!args.CanInteract || !args.CanAccess)
-            return;
-
-        if (!TryComp<ToolComponent>(args.User, out var tool) || !tool.Qualities.Contains(component.PryingQuality))
-            return;
-
-        args.Verbs.Add(new AlternativeVerb()
-        {
-            Text = Loc.GetString("door-pry"),
-            Impact = LogImpact.Low,
-            Act = () => TryPryDoor(uid, args.User, args.User, component, out _, force: true),
-        });
+        if (door.State == DoorState.Welded || !door.CanPry)
+            args.Cancelled = true;
     }
-
-
-    /// <summary>
-    ///     Pry open a door. This does not check if the user is holding the required tool.
-    /// </summary>
-    public bool TryPryDoor(EntityUid target, EntityUid tool, EntityUid user, DoorComponent door, out DoAfterId? id, bool force = false)
-    {
-        id = null;
-
-        if (door.State == DoorState.Welded)
-            return false;
-
-        if (!force)
-        {
-            var canEv = new BeforeDoorPryEvent(user, tool);
-            RaiseLocalEvent(target, canEv, false);
-
-            if (!door.CanPry || canEv.Cancelled)
-                // mark handled, as airlock component will cancel after generating a pop-up & you don't want to pry a tile
-                // under a windoor.
-                return true;
-        }
-
-        var modEv = new DoorGetPryTimeModifierEvent(user);
-        RaiseLocalEvent(target, modEv, false);
-
-        _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is using {ToPrettyString(tool)} to pry {ToPrettyString(target)} while it is {door.State}"); // TODO move to generic tool use logging in a way that includes door state
-        _toolSystem.UseTool(tool, user, target, TimeSpan.FromSeconds(modEv.PryTimeModifier * door.PryTime), new[] {door.PryingQuality}, new DoorPryDoAfterEvent(), out id);
-        return true; // we might not actually succeeded, but a do-after has started
-    }
-
-    private void OnPryFinished(EntityUid uid, DoorComponent door, DoAfterEvent args)
-    {
-        if (args.Cancelled)
-            return;
-
-        if (door.State == DoorState.Closed)
-        {
-            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open"); // TODO move to generic tool use logging in a way that includes door state
-            StartOpening(uid, door);
-        }
-        else if (door.State == DoorState.Open)
-        {
-            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed"); // TODO move to generic tool use logging in a way that includes door state
-            StartClosing(uid, door);
-        }
-    }
-#endregion
+    #endregion
 
 
     /// <summary>
@@ -233,7 +159,7 @@ public sealed class DoorSystem : SharedDoorSystem
     }
     private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
     {
-        if(TryComp<AirlockComponent>(uid, out var airlockComponent))
+        if (TryComp<AirlockComponent>(uid, out var airlockComponent))
         {
             if (_bolts.IsBolted(uid) || !this.IsPowered(uid, EntityManager))
                 return;
@@ -259,10 +185,27 @@ public sealed class DoorSystem : SharedDoorSystem
         if (door.OpenSound != null)
             PlaySound(uid, door.OpenSound, AudioParams.Default.WithVolume(-5), user, predicted);
 
-        if(lastState == DoorState.Emagging && TryComp<DoorBoltComponent>(uid, out var doorBoltComponent))
+        if (lastState == DoorState.Emagging && TryComp<DoorBoltComponent>(uid, out var doorBoltComponent))
             _bolts.SetBoltsWithAudio(uid, doorBoltComponent, !doorBoltComponent.BoltsDown);
     }
 
+    /// <summary>
+    ///     Open or close a door after it has been successfuly pried.
+    /// </summary>
+    private void OnAfterPry(EntityUid uid, DoorComponent door, ref PriedEvent args)
+    {
+        if (door.State == DoorState.Closed)
+        {
+            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open");
+            StartOpening(uid, door, args.User);
+        }
+        else if (door.State == DoorState.Open)
+        {
+            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed");
+            StartClosing(uid, door, args.User);
+        }
+    }
+
     protected override void CheckDoorBump(DoorComponent component, PhysicsComponent body)
     {
         var uid = body.Owner;
index 7147aa4f24c64eef381e669186d320d620b7f676..e2f25c63ab48b118f552ecce1487cd349cb23e05 100644 (file)
@@ -18,6 +18,7 @@ using Microsoft.Extensions.Options;
 using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Player;
+using Content.Shared.Prying.Components;
 
 namespace Content.Server.Doors.Systems
 {
@@ -38,7 +39,7 @@ namespace Content.Server.Doors.Systems
             base.Initialize();
 
             SubscribeLocalEvent<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
-            SubscribeLocalEvent<FirelockComponent, DoorGetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
+            SubscribeLocalEvent<FirelockComponent, GetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
             SubscribeLocalEvent<FirelockComponent, DoorStateChangedEvent>(OnUpdateState);
 
             SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
@@ -144,7 +145,7 @@ namespace Content.Server.Doors.Systems
                 args.Cancel();
         }
 
-        private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, DoorGetPryTimeModifierEvent args)
+        private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, ref GetPryTimeModifierEvent args)
         {
             var state = CheckPressureAndFire(uid, component);
 
@@ -261,7 +262,7 @@ namespace Content.Server.Doors.Systems
             List<AtmosDirection> directions = new(4);
             for (var i = 0; i < Atmospherics.Directions; i++)
             {
-                var dir = (AtmosDirection) (1 << i);
+                var dir = (AtmosDirection)(1 << i);
                 if (airtight.AirBlockedDirection.HasFlag(dir))
                 {
                     directions.Add(dir);
index 1d9c19de0276fe53bc9d76d27d68afd5b03414df..87deec9ea9dcb5b45b137d1c08eb4bc6de8bda11 100644 (file)
@@ -113,7 +113,7 @@ public sealed partial class NPCSteeringSystem
                         // TODO: Use the verb.
 
                         if (door.State != DoorState.Opening)
-                            _doors.TryPryDoor(ent, uid, uid, door, out id, force: true);
+                            _pryingSystem.TryPry(ent, uid, out id, uid);
 
                         component.DoAfterId = id;
                         return SteeringObstacleStatus.Continuing;
index cbc2ba6d2c4937d23c028ecc10b87c7afe2ea243..0fa28f6af795b8138ff5e18c845bca24a20c95ba 100644 (file)
@@ -31,6 +31,7 @@ using Robust.Shared.Random;
 using Robust.Shared.Threading;
 using Robust.Shared.Timing;
 using Robust.Shared.Utility;
+using Content.Shared.Prying.Systems;
 
 namespace Content.Server.NPC.Systems;
 
@@ -63,6 +64,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly SharedCombatModeSystem _combat = default!;
+    [Dependency] private readonly PryingSystem _pryingSystem = default!;
 
     private EntityQuery<FixturesComponent> _fixturesQuery;
     private EntityQuery<MovementSpeedModifierComponent> _modifierQuery;
@@ -148,7 +150,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
 
     private void OnDebugRequest(RequestNPCSteeringDebugEvent msg, EntitySessionEventArgs args)
     {
-        if (!_admin.IsAdmin((IPlayerSession) args.SenderSession))
+        if (!_admin.IsAdmin((IPlayerSession)args.SenderSession))
             return;
 
         if (msg.Enabled)
@@ -440,7 +442,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
         if (targetPoly != null &&
             steering.Coordinates.Position.Equals(Vector2.Zero) &&
             TryComp<PhysicsComponent>(uid, out var physics) &&
-            _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup) physics.CollisionMask))
+            _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup)physics.CollisionMask))
         {
             steering.CurrentPath.Clear();
             steering.CurrentPath.Enqueue(targetPoly);
index 5da7c6e8cd72167bce9846d9e0b80bec86a3df73..19e801322080412893a33daae923422696f13b5c 100644 (file)
@@ -32,6 +32,7 @@ using Content.Shared.Tools.Components;
 using Content.Shared.Weapons.Melee;
 using Content.Shared.Zombies;
 using Robust.Shared.Audio;
+using Content.Shared.Prying.Components;
 
 namespace Content.Server.Zombies
 {
@@ -162,11 +163,12 @@ namespace Content.Server.Zombies
                 melee.Damage = dspec;
 
                 // humanoid zombies get to pry open doors and shit
-                var tool = EnsureComp<ToolComponent>(target);
-                tool.SpeedModifier = 0.75f;
-                tool.Qualities = new ("Prying");
-                tool.UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg");
-                Dirty(tool);
+                var pryComp = EnsureComp<PryingComponent>(target);
+                pryComp.SpeedModifier = 0.75f;
+                pryComp.PryPowered = true;
+                pryComp.Force = true;
+
+                Dirty(target, pryComp);
             }
 
             Dirty(melee);
@@ -232,7 +234,7 @@ namespace Content.Server.Zombies
             else
             {
                 var htn = EnsureComp<HTNComponent>(target);
-                htn.RootTask = new HTNCompoundTask() {Task = "SimpleHostileCompound"};
+                htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
                 htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
                 _npc.WakeNPC(target, htn);
             }
index 567afa07701f90ce5b016586046ea2be3bf1c0ea..7cfcba8c5b61cac6ede179a9bab82ac2693809df 100644 (file)
@@ -249,7 +249,7 @@ public sealed partial class DoorComponent : Component
             }
 
             var curTime = IoCManager.Resolve<IGameTiming>().CurTime;
-            return (float) (NextStateChange.Value - curTime).TotalSeconds;
+            return (float)(NextStateChange.Value - curTime).TotalSeconds;
         }
         set
         {
@@ -299,10 +299,10 @@ public sealed partial class DoorComponent : Component
     public bool ClickOpen = true;
 
     [DataField("openDrawDepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
-    public int OpenDrawDepth = (int) DrawDepth.DrawDepth.Doors;
+    public int OpenDrawDepth = (int)DrawDepth.DrawDepth.Doors;
 
     [DataField("closedDrawDepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
-    public int ClosedDrawDepth = (int) DrawDepth.DrawDepth.Doors;
+    public int ClosedDrawDepth = (int)DrawDepth.DrawDepth.Doors;
 }
 
 [Serializable, NetSerializable]
index 5b0ca71ede71a513066c0b34bad7de8a03c60b56..08a2c8b18b103c102f0085644b6996dd7f60c706 100644 (file)
@@ -62,35 +62,4 @@ namespace Content.Shared.Doors
     public sealed class BeforeDoorAutoCloseEvent : CancellableEntityEventArgs
     {
     }
-
-    /// <summary>
-    /// Raised to determine how long the door's pry time should be modified by.
-    /// Multiply PryTimeModifier by the desired amount.
-    /// </summary>
-    public sealed class DoorGetPryTimeModifierEvent : EntityEventArgs
-    {
-        public readonly EntityUid User;
-        public float PryTimeModifier = 1.0f;
-
-        public DoorGetPryTimeModifierEvent(EntityUid user)
-        {
-            User = user;
-        }
-    }
-
-    /// <summary>
-    /// Raised when an attempt to pry open the door is made.
-    /// Cancel to stop the door from being pried open.
-    /// </summary>
-    public sealed class BeforeDoorPryEvent : CancellableEntityEventArgs
-    {
-        public readonly EntityUid User;
-        public readonly EntityUid Tool;
-
-        public BeforeDoorPryEvent(EntityUid user, EntityUid tool)
-        {
-            User = user;
-            Tool = tool;
-        }
-    }
 }
index e8be596b060e75fbf82b15c7608bbe15ef745a6c..1deb6e3f7c00837b51e4fdb836e2c47e3b5802ed 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Doors.Components;
 using Content.Shared.Popups;
+using Content.Shared.Prying.Components;
 
 namespace Content.Shared.Doors.Systems;
 
@@ -16,16 +17,16 @@ public abstract class SharedDoorBoltSystem : EntitySystem
         SubscribeLocalEvent<DoorBoltComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
         SubscribeLocalEvent<DoorBoltComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
         SubscribeLocalEvent<DoorBoltComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
-        SubscribeLocalEvent<DoorBoltComponent, BeforeDoorPryEvent>(OnDoorPry);
+        SubscribeLocalEvent<DoorBoltComponent, BeforePryEvent>(OnDoorPry);
 
     }
 
-    private void OnDoorPry(EntityUid uid, DoorBoltComponent component, BeforeDoorPryEvent args)
+    private void OnDoorPry(EntityUid uid, DoorBoltComponent component, ref BeforePryEvent args)
     {
-        if (component.BoltsDown)
+        if (component.BoltsDown && !args.Force)
         {
-            Popup.PopupEntity(Loc.GetString("airlock-component-cannot-pry-is-bolted-message"), uid, args.User);
-            args.Cancel();
+            Popup.PopupClient(Loc.GetString("airlock-component-cannot-pry-is-bolted-message"), uid, args.User);
+            args.Cancelled = true;
         }
     }
 
index 3fc912deba97e3f887b17f757dd1d3771987a28c..e5515171496b77023bd6b6388562959b5fa45dd2 100644 (file)
@@ -16,6 +16,7 @@ using Robust.Shared.Physics.Events;
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Serialization;
 using Robust.Shared.Timing;
+using Content.Shared.Prying.Components;
 
 namespace Content.Shared.Doors.Systems;
 
@@ -23,14 +24,14 @@ public abstract partial class SharedDoorSystem : EntitySystem
 {
     [Dependency] protected readonly IGameTiming GameTiming = default!;
     [Dependency] protected readonly SharedPhysicsSystem PhysicsSystem = default!;
-    [Dependency] private   readonly DamageableSystem _damageableSystem = default!;
-    [Dependency] private   readonly SharedStunSystem _stunSystem = default!;
+    [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+    [Dependency] private readonly SharedStunSystem _stunSystem = default!;
     [Dependency] protected readonly TagSystem Tags = default!;
     [Dependency] protected readonly SharedAudioSystem Audio = default!;
-    [Dependency] private   readonly EntityLookupSystem _entityLookup = default!;
+    [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
     [Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
-    [Dependency] private   readonly OccluderSystem _occluder = default!;
-    [Dependency] private   readonly AccessReaderSystem _accessReaderSystem = default!;
+    [Dependency] private readonly OccluderSystem _occluder = default!;
+    [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
 
     /// <summary>
     ///     A body must have an intersection percentage larger than this in order to be considered as colliding with a
@@ -61,6 +62,8 @@ public abstract partial class SharedDoorSystem : EntitySystem
 
         SubscribeLocalEvent<DoorComponent, StartCollideEvent>(HandleCollide);
         SubscribeLocalEvent<DoorComponent, PreventCollideEvent>(PreventCollision);
+        SubscribeLocalEvent<DoorComponent, GetPryTimeModifierEvent>(OnPryTimeModifier);
+
     }
 
     protected virtual void OnComponentInit(EntityUid uid, DoorComponent door, ComponentInit args)
@@ -182,6 +185,11 @@ public abstract partial class SharedDoorSystem : EntitySystem
         args.Handled = true;
     }
 
+    private void OnPryTimeModifier(EntityUid uid, DoorComponent door, ref GetPryTimeModifierEvent args)
+    {
+        args.BaseTime = door.PryTime;
+    }
+
     /// <summary>
     ///     Update the door state/visuals and play an access denied sound when a user without access interacts with the
     ///     door.
@@ -206,6 +214,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
             PlaySound(uid, door.DenySound, AudioParams.Default.WithVolume(-3), user, predicted);
     }
 
+
     public bool TryToggleDoor(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
     {
         if (!Resolve(uid, ref door))
@@ -246,7 +255,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
         if (door.State == DoorState.Welded)
             return false;
 
-        var ev = new BeforeDoorOpenedEvent(){User=user};
+        var ev = new BeforeDoorOpenedEvent() { User = user };
         RaiseLocalEvent(uid, ev, false);
         if (ev.Cancelled)
             return false;
@@ -261,6 +270,14 @@ public abstract partial class SharedDoorSystem : EntitySystem
         return true;
     }
 
+    /// <summary>
+    /// Immediately start opening a door
+    /// </summary>
+    /// <param name="uid"> The uid of the door</param>
+    /// <param name="door"> The doorcomponent of the door</param>
+    /// <param name="user"> The user (if any) opening the door</param>
+    /// <param name="predicted">Whether the interaction would have been
+    /// predicted. See comments in the PlaySound method on the Server system for details</param>
     public virtual void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
     {
         if (!Resolve(uid, ref door))
@@ -309,6 +326,14 @@ public abstract partial class SharedDoorSystem : EntitySystem
         return true;
     }
 
+    /// <summary>
+    /// Immediately start closing a door
+    /// </summary>
+    /// <param name="uid"> The uid of the door</param>
+    /// <param name="door"> The doorcomponent of the door</param>
+    /// <param name="user"> The user (if any) opening the door</param>
+    /// <param name="predicted">Whether the interaction would have been
+    /// predicted. See comments in the PlaySound method on the Server system for details</param>
     public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool quiet = true)
     {
         if (!Resolve(uid, ref door))
@@ -444,11 +469,11 @@ public abstract partial class SharedDoorSystem : EntitySystem
 
             //TODO: Make only shutters ignore these objects upon colliding instead of all airlocks
             // Excludes Glasslayer for windows, GlassAirlockLayer for windoors, TableLayer for tables
-            if (!otherPhysics.CanCollide || otherPhysics.CollisionLayer == (int) CollisionGroup.GlassLayer || otherPhysics.CollisionLayer == (int) CollisionGroup.GlassAirlockLayer || otherPhysics.CollisionLayer == (int) CollisionGroup.TableLayer)
+            if (!otherPhysics.CanCollide || otherPhysics.CollisionLayer == (int)CollisionGroup.GlassLayer || otherPhysics.CollisionLayer == (int)CollisionGroup.GlassAirlockLayer || otherPhysics.CollisionLayer == (int)CollisionGroup.TableLayer)
                 continue;
 
             //If the colliding entity is a slippable item ignore it by the airlock
-            if (otherPhysics.CollisionLayer == (int) CollisionGroup.SlipLayer && otherPhysics.CollisionMask == (int) CollisionGroup.ItemMask)
+            if (otherPhysics.CollisionLayer == (int)CollisionGroup.SlipLayer && otherPhysics.CollisionMask == (int)CollisionGroup.ItemMask)
                 continue;
 
             if ((physics.CollisionMask & otherPhysics.CollisionLayer) == 0 && (otherPhysics.CollisionMask & physics.CollisionLayer) == 0)
@@ -598,7 +623,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
         }
     }
 
-    protected virtual void CheckDoorBump(DoorComponent component, PhysicsComponent body) {}
+    protected virtual void CheckDoorBump(DoorComponent component, PhysicsComponent body) { }
 
     /// <summary>
     ///     Makes a door proceed to the next state (if applicable).
@@ -659,9 +684,4 @@ public abstract partial class SharedDoorSystem : EntitySystem
     #endregion
 
     protected abstract void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted);
-
-    [Serializable, NetSerializable]
-    protected sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent
-    {
-    }
 }
diff --git a/Content.Shared/Prying/Components/PryUnpoweredComponent.cs b/Content.Shared/Prying/Components/PryUnpoweredComponent.cs
new file mode 100644 (file)
index 0000000..f0e61dc
--- /dev/null
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Prying.Components;
+
+///<summary>
+/// Applied to entities that can be pried open without tools while unpowered
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PryUnpoweredComponent : Component
+{
+}
diff --git a/Content.Shared/Prying/Components/PryingComponent.cs b/Content.Shared/Prying/Components/PryingComponent.cs
new file mode 100644 (file)
index 0000000..4442481
--- /dev/null
@@ -0,0 +1,82 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Prying.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PryingComponent : Component
+{
+    /// <summary>
+    /// Whether the entity can pry open powered doors
+    /// </summary>
+    [DataField("pryPowered")]
+    public bool PryPowered = false;
+
+    /// <summary>
+    /// Whether the tool can bypass certain restrictions when prying.
+    /// For example door bolts.
+    /// </summary>
+    [DataField("force")]
+    public bool Force = false;
+    /// <summary>
+    /// Modifier on the prying time.
+    /// Lower values result in more time.
+    /// </summary>
+    [DataField("speedModifier")]
+    public float SpeedModifier = 1.0f;
+
+    /// <summary>
+    /// What sound to play when prying is finished.
+    /// </summary>
+    [DataField("useSound")]
+    public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg");
+
+    /// <summary>
+    /// Whether the entity can currently pry things.
+    /// </summary>
+    [DataField("enabled")]
+    public bool Enabled = true;
+}
+
+/// <summary>
+/// Raised directed on an entity before prying it.
+/// Cancel to stop the entity from being pried open.
+/// </summary>
+[ByRefEvent]
+public record struct BeforePryEvent(EntityUid User, bool PryPowered, bool Force)
+{
+    public readonly EntityUid User = User;
+
+    public readonly bool PryPowered = PryPowered;
+
+    public readonly bool Force = Force;
+
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Raised directed on an entity that has been pried.
+/// </summary>
+[ByRefEvent]
+public readonly record struct PriedEvent(EntityUid User)
+{
+    public readonly EntityUid User = User;
+}
+
+/// <summary>
+/// Raised to determine how long the door's pry time should be modified by.
+/// Multiply PryTimeModifier by the desired amount.
+/// </summary>
+[ByRefEvent]
+public record struct GetPryTimeModifierEvent
+{
+    public readonly EntityUid User;
+    public float PryTimeModifier = 1.0f;
+    public float BaseTime = 5.0f;
+
+    public GetPryTimeModifierEvent(EntityUid user)
+    {
+        User = user;
+    }
+}
+
diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs
new file mode 100644 (file)
index 0000000..3bb3a9b
--- /dev/null
@@ -0,0 +1,162 @@
+using Content.Shared.Prying.Components;
+using Content.Shared.Verbs;
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Database;
+using Content.Shared.Doors.Components;
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Interaction;
+using PryUnpoweredComponent = Content.Shared.Prying.Components.PryUnpoweredComponent;
+
+namespace Content.Shared.Prying.Systems;
+
+/// <summary>
+/// Handles prying of entities (e.g. doors)
+/// </summary>
+public sealed class PryingSystem : EntitySystem
+{
+    [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        // Mob prying doors
+        SubscribeLocalEvent<DoorComponent, GetVerbsEvent<AlternativeVerb>>(OnDoorAltVerb);
+        SubscribeLocalEvent<DoorComponent, DoorPryDoAfterEvent>(OnDoAfter);
+        SubscribeLocalEvent<DoorComponent, InteractUsingEvent>(TryPryDoor);
+    }
+
+    private void TryPryDoor(EntityUid uid, DoorComponent comp, InteractUsingEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        args.Handled = TryPry(uid, args.User, out _, args.Used);
+    }
+
+    private void OnDoorAltVerb(EntityUid uid, DoorComponent component, GetVerbsEvent<AlternativeVerb> args)
+    {
+        if (!args.CanInteract || !args.CanAccess)
+            return;
+
+        if (!TryComp<PryingComponent>(args.User, out var tool))
+            return;
+
+        args.Verbs.Add(new AlternativeVerb()
+        {
+            Text = Loc.GetString("door-pry"),
+            Impact = LogImpact.Low,
+            Act = () => TryPry(uid, args.User, out _, args.User),
+        });
+    }
+
+    /// <summary>
+    /// Attempt to pry an entity.
+    /// </summary>
+    public bool TryPry(EntityUid target, EntityUid user, out DoAfterId? id, EntityUid tool)
+    {
+        id = null;
+
+        PryingComponent? comp = null;
+        if (!Resolve(tool, ref comp, false))
+            return false;
+
+        if (!comp.Enabled)
+            return false;
+
+        if (!CanPry(target, user, comp))
+        {
+            // If we have reached this point we want the event that caused this
+            // to be marked as handled as a popup would be generated on failure.
+            return true;
+        }
+
+        StartPry(target, user, tool, comp.SpeedModifier, out id);
+
+        return true;
+    }
+
+    /// <summary>
+    /// Try to pry an entity.
+    /// </summary>
+    public bool TryPry(EntityUid target, EntityUid user, out DoAfterId? id)
+    {
+        id = null;
+
+        if (!CanPry(target, user))
+            // If we have reached this point we want the event that caused this
+            // to be marked as handled as a popup would be generated on failure.
+            return true;
+
+        return StartPry(target, user, null, 1.0f, out id);
+    }
+
+    private bool CanPry(EntityUid target, EntityUid user, PryingComponent? comp = null)
+    {
+        BeforePryEvent canev;
+
+        if (comp != null)
+        {
+            canev = new BeforePryEvent(user, comp.PryPowered, comp.Force);
+        }
+        else
+        {
+            if (!TryComp<PryUnpoweredComponent>(target, out _))
+                return false;
+            canev = new BeforePryEvent(user, false, false);
+        }
+
+        RaiseLocalEvent(target, ref canev);
+
+        if (canev.Cancelled)
+            return false;
+        return true;
+    }
+
+    private bool StartPry(EntityUid target, EntityUid user, EntityUid? tool, float toolModifier, [NotNullWhen(true)] out DoAfterId? id)
+    {
+        var modEv = new GetPryTimeModifierEvent(user);
+
+        RaiseLocalEvent(target, ref modEv);
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(modEv.BaseTime * modEv.PryTimeModifier / toolModifier), new DoorPryDoAfterEvent(), target, target, tool)
+        {
+            BreakOnDamage = true,
+            BreakOnUserMove = true,
+        };
+
+        if (tool != null)
+        {
+            _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is using {ToPrettyString(tool.Value)} to pry {ToPrettyString(target)}");
+        }
+        else
+        {
+            _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is prying {ToPrettyString(target)}");
+        }
+        return _doAfterSystem.TryStartDoAfter(doAfterArgs, out id);
+    }
+
+    private void OnDoAfter(EntityUid uid, DoorComponent door, DoorPryDoAfterEvent args)
+    {
+        if (args.Cancelled)
+            return;
+        if (args.Target is null)
+            return;
+
+        PryingComponent? comp = null;
+
+        if (args.Used != null && Resolve(args.Used.Value, ref comp))
+            _audioSystem.PlayPredicted(comp.UseSound, args.Used.Value, args.User);
+
+        var ev = new PriedEvent(args.User);
+        RaiseLocalEvent(uid, ref ev);
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent
+{
+}
index b198f6d779c81b9288888215c81d067ce7943ab4..d528c1be7dda69d5179f36e5d4a0f61ddd294f74 100644 (file)
@@ -2,6 +2,7 @@ using System.Linq;
 using Content.Shared.Interaction;
 using Content.Shared.Tools.Components;
 using Robust.Shared.GameStates;
+using Content.Shared.Prying.Components;
 
 namespace Content.Shared.Tools;
 
@@ -27,7 +28,7 @@ public abstract partial class SharedToolSystem : EntitySystem
     private void OnMultipleToolStartup(EntityUid uid, MultipleToolComponent multiple, ComponentStartup args)
     {
         // Only set the multiple tool if we have a tool component.
-        if(EntityManager.TryGetComponent(uid, out ToolComponent? tool))
+        if (EntityManager.TryGetComponent(uid, out ToolComponent? tool))
             SetMultipleTool(uid, multiple, tool);
     }
 
@@ -52,7 +53,7 @@ public abstract partial class SharedToolSystem : EntitySystem
         if (multiple.Entries.Length == 0)
             return false;
 
-        multiple.CurrentEntry = (uint) ((multiple.CurrentEntry + 1) % multiple.Entries.Length);
+        multiple.CurrentEntry = (uint)((multiple.CurrentEntry + 1) % multiple.Entries.Length);
         SetMultipleTool(uid, multiple, playSound: true, user: user);
 
         return true;
@@ -79,6 +80,19 @@ public abstract partial class SharedToolSystem : EntitySystem
         tool.UseSound = current.Sound;
         tool.Qualities = current.Behavior;
 
+        // TODO: Replace this with a better solution later
+        if (TryComp<PryingComponent>(uid, out var pcomp))
+        {
+            if (current.Behavior.Contains("Prying"))
+            {
+                pcomp.Enabled = true;
+            }
+            else
+            {
+                pcomp.Enabled = false;
+            }
+        }
+
         if (playSound && current.ChangeSound != null)
             _audioSystem.PlayPredicted(current.ChangeSound, uid, user);
 
index 66e2b35ad81c77acf5113fbc2981a9b346bad498..94464f2535debeeac79e464da0bd87135e7212ec 100644 (file)
@@ -24,6 +24,7 @@
   - type: Tool
     qualities:
       - Prying
+  - type: Prying
   - type: MultipleTool
     statusShowBehavior: true
     entries:
index 47ea55278fb47e2fc3f2a8b36c70ad6cda5d1f66..9831d20e27a9cccce00ca9d762219ba21cd27762 100644 (file)
     speed: 1.5
     qualities:
       - Prying
+  - type: Prying
+    pryPowered: !type:Bool
+        true
+    force: !type:Bool
+      true
     useSound:
       path: /Audio/Items/crowbar.ogg
   - type: Reactive
index d500ef141c30a5c7676a582a0bf3587361bfacbb..24eb0b02b2458799cc87ba7c706a02e849b00367 100644 (file)
@@ -1,4 +1,4 @@
-- type: entity
+- type: entity
   name: haycutters
   parent: BaseItem
   id: Haycutters
@@ -98,6 +98,7 @@
       path: /Audio/Items/crowbar.ogg
     speed: 0.05
   - type: TilePrying
+  - type: Prying
 
 - type: entity
   name: mooltitool
index a7f74a542f4d734da32dc703c071bff9cf2c3adb..36f96f61af884640ad610f86dd0c09738d93eb51 100644 (file)
       - Prying
     speed: 1.5
     useSound: /Audio/Items/jaws_pry.ogg
+  - type: Prying
+    pryPowered: !type:Bool
+      true
+    force: !type:Bool
+      true
+    useSound: /Audio/Items/jaws_pry.ogg
   - type: ToolForcePowered
   - type: MultipleTool
     statusShowBehavior: true
@@ -77,4 +83,4 @@
   - type: MeleeWeapon
     damage:
       types:
-        Blunt: 14
\ No newline at end of file
+        Blunt: 14
index 3d58b2b079d7fa67bc00ecf6fbb142b1248931dd..82bef616de47b74dcd5c93ed87d39956d9181017 100644 (file)
       Steel: 100
   - type: StaticPrice
     price: 22
+  - type: Prying
 
 - type: entity
   parent: Crowbar
index cb6f862745072e878c03a8236ac81a548f98234c..764683183d961634ba2a1fde46e3cc5e190aef73 100644 (file)
@@ -20,3 +20,4 @@
   - type: Tool\r
     qualities:\r
       - Prying\r
+  - type: Prying\r
index 9e747328f942517eedeebe7b390da7fa40d01601..fcc2129a51f588b2566973c49164aca94b4591c3 100644 (file)
@@ -38,6 +38,7 @@
       - Prying
   - type: TilePrying
     advanced: true
+  - type: Prying
 
 - type: entity
   id: FireAxeFlaming
index d182d9a00e91b4430369210be395c9597ef5ccc2..3073ed7ab53c2e396e4475e90ec2989c07d27ec6 100644 (file)
@@ -61,6 +61,7 @@
   - type: Tool
     qualities:
       - Prying
+  - type: Prying
 
 - type: entity
   name: crusher dagger
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/easy_pry.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/easy_pry.yml
new file mode 100644 (file)
index 0000000..04a58ee
--- /dev/null
@@ -0,0 +1,63 @@
+- type: entity
+  parent: AirlockExternal
+  id: AirlockExternalEasyPry
+  suffix: External, EasyPry
+  description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered
+
+- type: entity
+  parent: AirlockExternalGlass
+  id: AirlockExternalGlassEasyPry
+  suffix: External, Glass, EasyPry
+  description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered
+
+- type: entity
+  parent: AirlockGlassShuttle
+  id: AirlockGlassShuttleEasyPry
+  suffix: EasyPry, Docking
+  description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered
+
+- type: entity
+  parent: AirlockShuttle
+  id: AirlockShuttleEasyPry
+  suffix: EasyPry, Docking
+  description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered
+
+- type: entity
+  parent: AirlockExternalLocked
+  id: AirlockExternalEasyPryLocked
+  suffix: External, EasyPry, Locked
+  description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered
+
+- type: entity
+  parent: AirlockExternalGlassLocked
+  id: AirlockExternalGlassEasyPryLocked
+  suffix: External, Glass, EasyPry, Locked
+  description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered
+
+- type: entity
+  parent: AirlockExternalGlassShuttleLocked
+  id: AirlockGlassShuttleEasyPryLocked
+  suffix: EasyPry, Docking, Locked
+  description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered
+
+- type: entity
+  parent: AirlockExternalShuttleLocked
+  id: AirlockShuttleEasyPryLocked
+  suffix: EasyPry, Docking, Locked
+  description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN"
+  components:
+  - type: PryUnpowered