]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Pry open critical Borgs (#42319)
authorSir Warock <67167466+SirWarock@users.noreply.github.com>
Tue, 13 Jan 2026 22:08:45 +0000 (23:08 +0100)
committerGitHub <noreply@github.com>
Tue, 13 Jan 2026 22:08:45 +0000 (22:08 +0000)
* One commit ops

* Please the maintainer gods

* More requested changes

* review

* actually this is probably a good idea

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
Content.Shared/Lock/BypassLock/Components/BypassLockComponent.cs [new file with mode: 0644]
Content.Shared/Lock/BypassLock/Components/BypassLockRequiresMobStateComponent.cs [new file with mode: 0644]
Content.Shared/Lock/BypassLock/Systems/BypassLockSystem.MobState.cs [new file with mode: 0644]
Content.Shared/Lock/BypassLock/Systems/BypassLockSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/lock/bypass-lock-component.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml

diff --git a/Content.Shared/Lock/BypassLock/Components/BypassLockComponent.cs b/Content.Shared/Lock/BypassLock/Components/BypassLockComponent.cs
new file mode 100644 (file)
index 0000000..38c6275
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Shared.Lock.BypassLock.Systems;
+using Content.Shared.Tools;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Lock.BypassLock.Components;
+
+/// <summary>
+/// This component lets the lock on this entity be pried open when the entity is in critical or dead state.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(BypassLockSystem))]
+public sealed partial class BypassLockComponent : Component
+{
+    /// <summary>
+    /// The tool quality needed to bypass the lock.
+    /// </summary>
+    [DataField]
+    public ProtoId<ToolQualityPrototype> BypassingTool = "Prying";
+
+    /// <summary>
+    /// Amount of time in seconds it takes to bypass
+    /// </summary>
+    [DataField]
+    public TimeSpan BypassDelay = TimeSpan.FromSeconds(5f);
+}
diff --git a/Content.Shared/Lock/BypassLock/Components/BypassLockRequiresMobStateComponent.cs b/Content.Shared/Lock/BypassLock/Components/BypassLockRequiresMobStateComponent.cs
new file mode 100644 (file)
index 0000000..7e40a8f
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Shared.Lock.BypassLock.Systems;
+using Content.Shared.Mobs;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Lock.BypassLock.Components;
+
+/// <summary>
+/// This component lets the lock on this entity be pried open when the entity is in critical or dead state.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(BypassLockSystem))]
+public sealed partial class BypassLockRequiresMobStateComponent : Component
+{
+    /// <summary>
+    /// The mobstate where the lock can be bypassed.
+    /// </summary>
+    [DataField]
+    public HashSet<MobState> RequiredMobState = [];
+}
diff --git a/Content.Shared/Lock/BypassLock/Systems/BypassLockSystem.MobState.cs b/Content.Shared/Lock/BypassLock/Systems/BypassLockSystem.MobState.cs
new file mode 100644 (file)
index 0000000..02ec502
--- /dev/null
@@ -0,0 +1,41 @@
+using Content.Shared.Lock.BypassLock.Components;
+using Content.Shared.Mobs.Components;
+
+namespace Content.Shared.Lock.BypassLock.Systems;
+
+public sealed partial class BypassLockSystem
+{
+    private void InitializeMobStateLockSystem()
+    {
+        SubscribeLocalEvent<BypassLockRequiresMobStateComponent, ForceOpenLockAttemptEvent>(OnForceOpenLockAttempt);
+        SubscribeLocalEvent<BypassLockRequiresMobStateComponent, CheckBypassLockVerbRequirements>(OnGetVerb);
+    }
+
+    private void OnForceOpenLockAttempt(Entity<BypassLockRequiresMobStateComponent> target, ref ForceOpenLockAttemptEvent args)
+    {
+        if (!TryComp<MobStateComponent>(target, out var mobState))
+            return;
+
+        args.CanForceOpen &= target.Comp.RequiredMobState.Contains(mobState.CurrentState);
+    }
+
+    private void OnGetVerb(Entity<BypassLockRequiresMobStateComponent> target, ref CheckBypassLockVerbRequirements args)
+    {
+        if (!TryComp<MobStateComponent>(target, out var mobState))
+            return;
+
+        // Only show disabled verb on a too healthy target when they have the right tool.
+        if (!target.Comp.RequiredMobState.Contains(mobState.CurrentState) && args.RightTool)
+        {
+            args.Verb.Disabled = true;
+            args.Verb.Message = Loc.GetString("bypass-lock-disabled-healthy");
+        }
+        // Show verb of using the wrong tool when the target is critical.
+        else if (target.Comp.RequiredMobState.Contains(mobState.CurrentState) && !args.RightTool)
+        {
+            args.ShowVerb = true;
+            args.Verb.Disabled = true;
+            args.Verb.Message = Loc.GetString("bypass-lock-disabled-wrong-tool", ("quality", args.ToolQuality.ToString().ToLower()));
+        }
+    }
+}
diff --git a/Content.Shared/Lock/BypassLock/Systems/BypassLockSystem.cs b/Content.Shared/Lock/BypassLock/Systems/BypassLockSystem.cs
new file mode 100644 (file)
index 0000000..1dcc4ee
--- /dev/null
@@ -0,0 +1,132 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Interaction;
+using Content.Shared.Lock.BypassLock.Components;
+using Content.Shared.Tools;
+using Content.Shared.Tools.Systems;
+using Content.Shared.Verbs;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Lock.BypassLock.Systems;
+
+public sealed partial class BypassLockSystem : EntitySystem
+{
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly LockSystem _lock = default!;
+    [Dependency] private readonly SharedToolSystem _tool = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<BypassLockComponent, InteractUsingEvent>(OnInteractUsing);
+        SubscribeLocalEvent<LockComponent, ForceOpenLockDoAfterEvent>(OnBypassAccessDoAfterEvent);
+        SubscribeLocalEvent<BypassLockComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
+
+        InitializeMobStateLockSystem();
+    }
+
+    private void OnInteractUsing(Entity<BypassLockComponent> target, ref InteractUsingEvent args)
+    {
+        if (target.Owner == args.User)
+            return;
+        
+        if (!_tool.HasQuality(args.Used, target.Comp.BypassingTool)
+            || !_lock.IsLocked(target.Owner))
+            return;
+
+        var ev = new ForceOpenLockAttemptEvent(args.User);
+        RaiseLocalEvent(target.Owner, ref ev);
+
+        if (!ev.CanForceOpen)
+            return;
+
+        args.Handled = TryStartDoAfter(target, args.User, args.Used);
+    }
+
+    private bool TryStartDoAfter(Entity<BypassLockComponent> target, EntityUid user, EntityUid used)
+    {
+        if (!_tool.UseTool(
+                used,
+                user,
+                target,
+                (float) target.Comp.BypassDelay.TotalSeconds,
+                target.Comp.BypassingTool,
+                new ForceOpenLockDoAfterEvent()))
+        {
+            return false;
+        }
+
+        _adminLogger.Add(LogType.Action, LogImpact.Low,
+            $"{ToPrettyString(user):user} is prying {ToPrettyString(target):target}'s lock open at {Transform(target).Coordinates:targetlocation}");
+        return true;
+    }
+
+    private void OnBypassAccessDoAfterEvent(Entity<LockComponent> target, ref ForceOpenLockDoAfterEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        _lock.Unlock(target, args.User, target.Comp);
+    }
+
+    private void OnGetVerb(Entity<BypassLockComponent> target, ref GetVerbsEvent<InteractionVerb> args)
+    {
+        if (!args.CanInteract || !args.CanAccess || args.Using == null)
+            return;
+
+        var rightTool = _tool.HasQuality(args.Using.Value, target.Comp.BypassingTool);
+        var item = args.Using.Value;
+        var bypassVerb = new InteractionVerb
+        {
+            IconEntity = GetNetEntity(item),
+        };
+
+        bypassVerb.Text = bypassVerb.Message = Loc.GetString("bypass-lock-verb");
+
+        var ev = new CheckBypassLockVerbRequirements(bypassVerb, rightTool, rightTool, target.Comp.BypassingTool);
+        RaiseLocalEvent(target, ref ev);
+
+        if (!ev.ShowVerb)
+            return;
+
+        var user = args.User;
+
+        bypassVerb.Act = () => TryStartDoAfter(target, user, item);
+
+        if (!_lock.IsLocked(target.Owner))
+        {
+            bypassVerb.Disabled = true;
+            bypassVerb.Message = Loc.GetString("bypass-lock-disabled-already-open");
+        }
+
+        args.Verbs.Add(bypassVerb);
+    }
+}
+
+/// <summary>
+/// This event gets raised on the entity with the <see cref="BypassLockRequiresMobStateComponent"/> after someone finished
+/// a doafter forcing the lock open.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class ForceOpenLockDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// This gets raised on the target whose lock is attempted to be forced open.
+/// </summary>
+/// <param name="User">Entity attempting to open this.</param>
+/// <param name="CanForceOpen">Whether the lock can be forced open.</param>
+[ByRefEvent]
+public record struct ForceOpenLockAttemptEvent(EntityUid User, bool CanForceOpen = true);
+
+/// <summary>
+/// This gets raised on the target that is being right-clicked to check for verb requirements.
+/// </summary>
+/// <param name="Verb">The interaction verb that will be shown.</param>
+/// <param name="RightTool">Whether the tool has the right properties to force the lock open.</param>
+/// <param name="ShowVerb">Whether the verb should be shown.</param>
+/// <param name="ToolQuality">The required tool quality to force the lock open.</param>
+[ByRefEvent]
+public record struct CheckBypassLockVerbRequirements(InteractionVerb Verb, bool RightTool, bool ShowVerb, ProtoId<ToolQualityPrototype> ToolQuality);
diff --git a/Resources/Locale/en-US/lock/bypass-lock-component.ftl b/Resources/Locale/en-US/lock/bypass-lock-component.ftl
new file mode 100644 (file)
index 0000000..d379960
--- /dev/null
@@ -0,0 +1,4 @@
+bypass-lock-verb = Force open the access lock
+bypass-lock-disabled-healthy = The lock needs to be damaged further before it can be forced open.
+bypass-lock-disabled-wrong-tool = This lock requires {$quality} to be forced open.
+bypass-lock-disabled-already-open = The lock is already open.
index 681cb86796eb7459e77df869d49eae9b29fe89fa..7b2adf1a2240af9827670bd5769b592f4a821024 100644 (file)
       - BorgChassis
       - RoboticsConsole
   - type: WiresPanel
-    openingTool: Prying
   - type: ActivatableUIRequiresPanel
   - type: NameIdentifier
     group: Silicon
     damageProtection:
       flatReductions:
         Heat: 10 # capable of touching light bulbs and stoves without feeling pain!
+  - type: BypassLock
+  - type: BypassLockRequiresMobState
+    requiredMobState:
+    - Critical
+    - Dead
 
 - type: entity
   abstract: true
       Unsexed: UnisexSiliconSyndicate
   - type: PointLight
     color: "#dd200b"
+  - type: BypassLock
+    bypassDelay: 15 # We don't want people to easily be able to remove syndieborg brains.
 
 - type: entity
   id: BaseBorgChassisDerelict
       Unsexed: UnisexSiliconSyndicate
   - type: PointLight
     color: "#dd200b"
+  - type: BypassLock
+    bypassDelay: 15 # We don't want people to easily be able to remove syndieborg brains.
 
 - type: entity
   parent: BaseBorgChassisNotIonStormable
     interactSuccessSound:
       path: /Audio/Ambience/Objects/periodic_beep.ogg
   - type: Xenoborg
+  - type: BypassLock
+    bypassDelay: 15 # We don't want people to easily be able to remove xenoborg brains.