]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Event based lock access (#40883)
authorScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Fri, 17 Oct 2025 02:04:43 +0000 (04:04 +0200)
committerGitHub <noreply@github.com>
Fri, 17 Oct 2025 02:04:43 +0000 (02:04 +0000)
* init

* some bonus stuff

* CheckForAnyReaders

* reader

* doc

* review

* fuck yaml

* Me when I push changes myshelf

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Content.Shared/Access/Systems/AccessReaderSystem.cs
Content.Shared/Delivery/SharedDeliverySystem.cs
Content.Shared/FingerprintReader/FingerprintReaderComponent.cs
Content.Shared/FingerprintReader/FingerprintReaderSystem.cs
Content.Shared/Lock/LockComponent.cs
Content.Shared/Lock/LockSystem.cs
Resources/Prototypes/Entities/Objects/Deliveries/deliveries.yml

index 801bfd4b1ddbc7a8a85b30c6e954f474db8c5472..f8c6d4924415bcdcc332232331dcb1b28fc4750c 100644 (file)
@@ -10,6 +10,7 @@ using Content.Shared.Hands.EntitySystems;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Inventory;
 using Content.Shared.Localizations;
+using Content.Shared.Lock;
 using Content.Shared.NameIdentifier;
 using Content.Shared.PDA;
 using Content.Shared.StationRecords;
@@ -44,6 +45,8 @@ public sealed class AccessReaderSystem : EntitySystem
         SubscribeLocalEvent<AccessReaderComponent, GotEmaggedEvent>(OnEmagged);
         SubscribeLocalEvent<AccessReaderComponent, LinkAttemptEvent>(OnLinkAttempt);
         SubscribeLocalEvent<AccessReaderComponent, AccessReaderConfigurationAttemptEvent>(OnConfigurationAttempt);
+        SubscribeLocalEvent<AccessReaderComponent, FindAvailableLocksEvent>(OnFindAvailableLocks);
+        SubscribeLocalEvent<AccessReaderComponent, CheckUserHasLockAccessEvent>(OnCheckLockAccess);
 
         SubscribeLocalEvent<AccessReaderComponent, ComponentGetState>(OnGetState);
         SubscribeLocalEvent<AccessReaderComponent, ComponentHandleState>(OnHandleState);
@@ -169,6 +172,22 @@ public sealed class AccessReaderSystem : EntitySystem
         ent.Comp.AccessListsOriginal ??= new(ent.Comp.AccessLists);
     }
 
+    private void OnFindAvailableLocks(Entity<AccessReaderComponent> ent, ref FindAvailableLocksEvent args)
+    {
+        args.FoundReaders |= LockTypes.Access;
+    }
+
+    private void OnCheckLockAccess(Entity<AccessReaderComponent> ent, ref CheckUserHasLockAccessEvent args)
+    {
+        // Are we looking for an access lock?
+        if (!args.FoundReaders.HasFlag(LockTypes.Access))
+            return;
+
+        // If the user has access to this lock, we pass it into the event.
+        if (IsAllowed(args.User, ent))
+            args.HasAccess |= LockTypes.Access;
+    }
+
     /// <summary>
     /// Searches the source for access tags
     /// then compares it with the all targets accesses to see if it is allowed.
index d7fc40dcc66f97476e1390fd787a6ff33f55ca90..71baa92ec67490b534f6fd6f5bac5399f581aa06 100644 (file)
@@ -162,7 +162,7 @@ public abstract class SharedDeliverySystem : EntitySystem
     private bool TryUnlockDelivery(Entity<DeliveryComponent> ent, EntityUid user, bool rewardMoney = true, bool force = false)
     {
         // Check fingerprint access if there is a reader on the mail
-        if (!force && TryComp<FingerprintReaderComponent>(ent, out var reader) && !_fingerprintReader.IsAllowed((ent, reader), user))
+        if (!force && !_fingerprintReader.IsAllowed(ent.Owner, user, out _))
             return false;
 
         var deliveryName = _nameModifier.GetBaseName(ent.Owner);
index 166551cfe72f66fa4612156396ea6675a4eb242e..2f8a9232ff4cd3b5323a6310989e1471d971a5fa 100644 (file)
@@ -20,16 +20,4 @@ public sealed partial class FingerprintReaderComponent : Component
     /// </summary>
     [DataField, AutoNetworkedField]
     public bool IgnoreGloves;
-
-    /// <summary>
-    /// The popup to show when access is denied due to fingerprint mismatch.
-    /// </summary>
-    [DataField]
-    public LocId? FailPopup;
-
-    /// <summary>
-    /// The popup to show when access is denied due to wearing gloves.
-    /// </summary>
-    [DataField]
-    public LocId? FailGlovesPopup;
 }
index aa7d190c34c552e3a298ae102e18b60a3e3b344b..73b06cac9b5c6ee73bbb4fdcf456b39f25d8c461 100644 (file)
@@ -1,6 +1,7 @@
 using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Forensics.Components;
 using Content.Shared.Inventory;
+using Content.Shared.Lock;
 using Content.Shared.Popups;
 using JetBrains.Annotations;
 
@@ -12,15 +13,45 @@ public sealed class FingerprintReaderSystem : EntitySystem
     [Dependency] private readonly InventorySystem _inventory = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
 
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<FingerprintReaderComponent, FindAvailableLocksEvent>(OnFindAvailableLocks);
+        SubscribeLocalEvent<FingerprintReaderComponent, CheckUserHasLockAccessEvent>(OnCheckLockAccess);
+    }
+
+    private void OnFindAvailableLocks(Entity<FingerprintReaderComponent> ent, ref FindAvailableLocksEvent args)
+    {
+        args.FoundReaders |= LockTypes.Fingerprint;
+    }
+
+    private void OnCheckLockAccess(Entity<FingerprintReaderComponent> ent, ref CheckUserHasLockAccessEvent args)
+    {
+        // Are we looking for a fingerprint lock?
+        if (!args.FoundReaders.HasFlag(LockTypes.Fingerprint))
+            return;
+
+        // If the user has access to this lock, we pass it into the event.
+        if (IsAllowed(ent.Owner, args.User, out var denyReason))
+            args.HasAccess |= LockTypes.Fingerprint;
+        else
+            args.DenyReason = denyReason;
+    }
+
     /// <summary>
     /// Checks if the given user has fingerprint access to the target entity.
     /// </summary>
     /// <param name="target">The target entity.</param>
     /// <param name="user">User trying to gain access.</param>
+    /// <param name="showPopup">Whether to display a popup with the reason you are not allowed to access this.</param>
+    /// <param name="denyReason">The reason why access was denied.</param>
     /// <returns>True if access was granted, otherwise false.</returns>
+    // TODO: Remove showPopup, just keeping it here for backwards compatibility while I refactor mail
     [PublicAPI]
-    public bool IsAllowed(Entity<FingerprintReaderComponent?> target, EntityUid user, bool showPopup = true)
+    public bool IsAllowed(Entity<FingerprintReaderComponent?> target, EntityUid user, [NotNullWhen(false)] out string? denyReason, bool showPopup = true)
     {
+        denyReason = null;
         if (!Resolve(target, ref target.Comp, false))
             return true;
 
@@ -30,8 +61,11 @@ public sealed class FingerprintReaderSystem : EntitySystem
         // Check for gloves first
         if (!target.Comp.IgnoreGloves && TryGetBlockingGloves(user, out var gloves))
         {
-            if (target.Comp.FailGlovesPopup != null && showPopup)
-                _popup.PopupClient(Loc.GetString(target.Comp.FailGlovesPopup, ("blocker", gloves)), target, user);
+            denyReason = Loc.GetString("fingerprint-reader-fail-gloves", ("blocker", gloves));
+
+            if (showPopup)
+                _popup.PopupClient(denyReason, target, user);
+
             return false;
         }
 
@@ -39,8 +73,10 @@ public sealed class FingerprintReaderSystem : EntitySystem
         if (!TryComp<FingerprintComponent>(user, out var fingerprint) || fingerprint.Fingerprint == null ||
             !target.Comp.AllowedFingerprints.Contains(fingerprint.Fingerprint))
         {
-            if (target.Comp.FailPopup != null && showPopup)
-                _popup.PopupClient(Loc.GetString(target.Comp.FailPopup), target, user);
+            denyReason = Loc.GetString("fingerprint-reader-fail");
+
+            if (showPopup)
+                _popup.PopupClient(denyReason, target, user);
 
             return false;
         }
index 1e5f0fdd50d199c811243a96541ef539e309b9f0..822ec78f9b58b17d03d5e1c5eb98c96b985da6dd 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Shared.Access.Components;
 using Content.Shared.DoAfter;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
@@ -47,11 +46,37 @@ public sealed partial class LockComponent : Component
     public bool UnlockOnClick = true;
 
     /// <summary>
-    /// Whether the lock requires access validation through <see cref="AccessReaderComponent"/>
+    /// Whether or not the lock is locked when used it hand.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool LockInHand;
+
+    /// <summary>
+    /// Whether or not the lock is unlocked when used in hand.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool UnlockInHand;
+
+    /// <summary>
+    /// Whether access requirements should be checked for this lock.
     /// </summary>
     [DataField, AutoNetworkedField]
     public bool UseAccess = true;
 
+    /// <summary>
+    /// What readers should be checked to determine if an entity has access.
+    /// If null, all possible readers are checked.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public LockTypes? CheckedLocks;
+
+    /// <summary>
+    /// Whether any reader needs to be accessed to operate this lock.
+    /// By default, all readers need to be able to be accessed.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool CheckForAnyReaders;
+
     /// <summary>
     /// The sound played when unlocked.
     /// </summary>
@@ -96,6 +121,12 @@ public sealed partial class LockComponent : Component
     [DataField]
     [AutoNetworkedField]
     public TimeSpan UnlockTime;
+
+    /// <summary>
+    /// Whether this lock can be locked again after being unlocked.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool AllowRepeatedLocking = true;
 }
 
 /// <summary>
index 2abb45d8788f3c99c1afa8cec99bf6d6e0b2ff80..ca780780fb53d0335210f397a1b921db0c48c2b1 100644 (file)
@@ -1,5 +1,3 @@
-using Content.Shared.Access.Components;
-using Content.Shared.Access.Systems;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Construction.Components;
 using Content.Shared.DoAfter;
@@ -7,6 +5,7 @@ using Content.Shared.Emag.Systems;
 using Content.Shared.Examine;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
 using Content.Shared.Popups;
 using Content.Shared.Storage;
 using Content.Shared.Storage.Components;
@@ -16,6 +15,7 @@ using Content.Shared.Wires;
 using Content.Shared.Item.ItemToggle.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Audio.Systems;
+using Robust.Shared.Serialization;
 using Robust.Shared.Utility;
 
 namespace Content.Shared.Lock;
@@ -26,7 +26,6 @@ namespace Content.Shared.Lock;
 [UsedImplicitly]
 public sealed class LockSystem : EntitySystem
 {
-    [Dependency] private readonly AccessReaderSystem _accessReader = default!;
     [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
     [Dependency] private readonly EmagSystem _emag = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
@@ -35,6 +34,8 @@ public sealed class LockSystem : EntitySystem
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
 
+    private readonly LocId _defaultDenyReason = "lock-comp-has-user-access-fail";
+
     /// <inheritdoc />
     public override void Initialize()
     {
@@ -42,6 +43,7 @@ public sealed class LockSystem : EntitySystem
 
         SubscribeLocalEvent<LockComponent, ComponentStartup>(OnStartup);
         SubscribeLocalEvent<LockComponent, ActivateInWorldEvent>(OnActivated, before: [typeof(ActivatableUISystem)]);
+        SubscribeLocalEvent<LockComponent, UseInHandEvent>(OnUseInHand, before: [typeof(ActivatableUISystem)]);
         SubscribeLocalEvent<LockComponent, StorageOpenAttemptEvent>(OnStorageOpenAttempt);
         SubscribeLocalEvent<LockComponent, ExaminedEvent>(OnExamined);
         SubscribeLocalEvent<LockComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleLockVerb);
@@ -84,6 +86,23 @@ public sealed class LockSystem : EntitySystem
         }
     }
 
+    private void OnUseInHand(EntityUid uid, LockComponent lockComp, UseInHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        if (lockComp.Locked && lockComp.UnlockInHand)
+        {
+            args.Handled = true;
+            TryUnlock(uid, args.User, lockComp);
+        }
+        else if (!lockComp.Locked && lockComp.LockInHand)
+        {
+            args.Handled = true;
+            TryLock(uid, args.User, lockComp);
+        }
+    }
+
     private void OnStorageOpenAttempt(EntityUid uid, LockComponent component, ref StorageOpenAttemptEvent args)
     {
         if (!component.Locked)
@@ -125,7 +144,7 @@ public sealed class LockSystem : EntitySystem
         if (!CanToggleLock(uid, user, quiet: false))
             return false;
 
-        if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false))
+        if (lockComp.UseAccess && !HasUserAccess(uid, user, false))
             return false;
 
         if (!skipDoAfter && lockComp.LockTime != TimeSpan.Zero)
@@ -224,7 +243,7 @@ public sealed class LockSystem : EntitySystem
         if (!CanToggleLock(uid, user, quiet: false))
             return false;
 
-        if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false))
+        if (lockComp.UseAccess && !HasUserAccess(uid, user, false))
             return false;
 
         if (!skipDoAfter && lockComp.UnlockTime != TimeSpan.Zero)
@@ -273,33 +292,69 @@ public sealed class LockSystem : EntitySystem
     /// Raises an event for other components to check whether or not
     /// the entity can be locked in its current state.
     /// </summary>
-    public bool CanToggleLock(EntityUid uid, EntityUid user, bool quiet = true)
+    public bool CanToggleLock(Entity<LockComponent?> ent, EntityUid user, bool quiet = true)
     {
+        if (!Resolve(ent, ref ent.Comp))
+            return false;
+
         if (!_actionBlocker.CanComplexInteract(user))
             return false;
 
+        if (!ent.Comp.Locked && !ent.Comp.AllowRepeatedLocking)
+            return false;
+
         var ev = new LockToggleAttemptEvent(user, quiet);
-        RaiseLocalEvent(uid, ref ev, true);
+        RaiseLocalEvent(ent, ref ev, true);
         if (ev.Cancelled)
             return false;
 
-        var userEv = new UserLockToggleAttemptEvent(uid, quiet);
+        var userEv = new UserLockToggleAttemptEvent(ent, quiet);
         RaiseLocalEvent(user, ref userEv, true);
         return !userEv.Cancelled;
     }
 
-    // TODO: this should be a helper on AccessReaderSystem since so many systems copy paste it
-    private bool HasUserAccess(EntityUid uid, EntityUid user, AccessReaderComponent? reader = null, bool quiet = true)
+    /// <summary>
+    /// Checks whether the user has access to locks on an entity.
+    /// </summary>
+    /// <param name="ent">The entity we check for locks.</param>
+    /// <param name="user">The user we check for access.</param>
+    /// <param name="quiet">Whether to display a popup if user has no access.</param>
+    /// <returns>True if the user has access, otherwise False.</returns>
+    [PublicAPI]
+    public bool HasUserAccess(Entity<LockComponent?> ent, EntityUid user, bool quiet = true)
     {
-        // Not having an AccessComponent means you get free access. woo!
-        if (!Resolve(uid, ref reader, false))
+        // Entity literally has no lock. Congratulations.
+        if (!Resolve(ent, ref ent.Comp, false))
             return true;
 
-        if (_accessReader.IsAllowed(user, uid, reader))
+        var checkedReaders = LockTypes.None;
+        if (ent.Comp.CheckedLocks is null)
+        {
+            var lockEv = new FindAvailableLocksEvent(user);
+            RaiseLocalEvent(ent, ref lockEv);
+            checkedReaders = lockEv.FoundReaders;
+        }
+
+        // If no locks are found, you have access. Woo!
+        if (checkedReaders == LockTypes.None)
+            return true;
+
+        var accessEv = new CheckUserHasLockAccessEvent(user, checkedReaders);
+        RaiseLocalEvent(ent, ref accessEv);
+
+        // If we check for any, as long as user has access to any of the locks we grant access.
+        if (accessEv.HasAccess != LockTypes.None && ent.Comp.CheckForAnyReaders)
+            return true;
+
+        if (accessEv.HasAccess == checkedReaders)
             return true;
 
         if (!quiet)
-            _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-has-user-access-fail"), uid, user);
+        {
+            var denyReason = accessEv.DenyReason ?? _defaultDenyReason;
+            _sharedPopupSystem.PopupClient(denyReason, ent, user);
+        }
+
         return false;
     }
 
@@ -466,3 +521,35 @@ public sealed class LockSystem : EntitySystem
         }
     }
 }
+
+/// <summary>
+/// Raised on an entity to check whether it has any readers that can prevent it from being opened.
+/// </summary>
+/// <param name="User">The person attempting to open the entity.</param>
+/// <param name="FoundReaders">What readers were found. This should not be set when raising the event.</param>
+[ByRefEvent]
+public record struct FindAvailableLocksEvent(EntityUid User, LockTypes FoundReaders = LockTypes.None);
+
+/// <summary>
+/// Raised on an entity to check if the user has access (ID, Fingerprint, etc) to said entity.
+/// </summary>
+/// <param name="User">The user we are checking.</param>
+/// <param name="FoundReaders">What readers we are attempting to verify access for.</param>
+/// <param name="HasAccess">Which readers the user has access to. This should not be set when raising the event.</param>
+[ByRefEvent]
+public record struct CheckUserHasLockAccessEvent(EntityUid User, LockTypes FoundReaders = LockTypes.None, LockTypes HasAccess = LockTypes.None, string? DenyReason = null);
+
+/// <summary>
+/// Enum of all readers a lock can be "locked" by.
+/// Used to determine what you need in order to access the lock.
+/// For example, an entity with <see cref="AccessReaderComponent"/> will have the Access type, which is gathered by an event and handled by the respective system.
+/// </summary>
+[Flags]
+[Serializable, NetSerializable]
+public enum LockTypes : byte
+{
+    None, // Default state, means the lock is not restricted.
+    Access, // Means there is an AccessReader currently present.
+    Fingerprint, // Means there is a FingerprintReader currently present.
+    All = Access | Fingerprint,
+}
index 0c33f53881561568cf45039b7b549c26647573b3..a011460366c58edd8642082d755ccd868ec42f7e 100644 (file)
@@ -39,8 +39,6 @@
   - type: Label
     examinable: false
   - type: FingerprintReader
-    failPopup: fingerprint-reader-fail
-    failGlovesPopup: fingerprint-reader-fail-gloves
   - type: Delivery
   - type: DeliveryRandomMultiplier
   - type: ContainerContainer