diff --git a/Content.Shared/Access/Systems/AccessReaderSystem.cs b/Content.Shared/Access/Systems/AccessReaderSystem.cs index 801bfd4b1d..f8c6d49244 100644 --- a/Content.Shared/Access/Systems/AccessReaderSystem.cs +++ b/Content.Shared/Access/Systems/AccessReaderSystem.cs @@ -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(OnEmagged); SubscribeLocalEvent(OnLinkAttempt); SubscribeLocalEvent(OnConfigurationAttempt); + SubscribeLocalEvent(OnFindAvailableLocks); + SubscribeLocalEvent(OnCheckLockAccess); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); @@ -169,6 +172,22 @@ public sealed class AccessReaderSystem : EntitySystem ent.Comp.AccessListsOriginal ??= new(ent.Comp.AccessLists); } + private void OnFindAvailableLocks(Entity ent, ref FindAvailableLocksEvent args) + { + args.FoundReaders |= LockTypes.Access; + } + + private void OnCheckLockAccess(Entity 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; + } + /// /// Searches the source for access tags /// then compares it with the all targets accesses to see if it is allowed. diff --git a/Content.Shared/Delivery/SharedDeliverySystem.cs b/Content.Shared/Delivery/SharedDeliverySystem.cs index d7fc40dcc6..71baa92ec6 100644 --- a/Content.Shared/Delivery/SharedDeliverySystem.cs +++ b/Content.Shared/Delivery/SharedDeliverySystem.cs @@ -162,7 +162,7 @@ public abstract class SharedDeliverySystem : EntitySystem private bool TryUnlockDelivery(Entity ent, EntityUid user, bool rewardMoney = true, bool force = false) { // Check fingerprint access if there is a reader on the mail - if (!force && TryComp(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); diff --git a/Content.Shared/FingerprintReader/FingerprintReaderComponent.cs b/Content.Shared/FingerprintReader/FingerprintReaderComponent.cs index 166551cfe7..2f8a9232ff 100644 --- a/Content.Shared/FingerprintReader/FingerprintReaderComponent.cs +++ b/Content.Shared/FingerprintReader/FingerprintReaderComponent.cs @@ -20,16 +20,4 @@ public sealed partial class FingerprintReaderComponent : Component /// [DataField, AutoNetworkedField] public bool IgnoreGloves; - - /// - /// The popup to show when access is denied due to fingerprint mismatch. - /// - [DataField] - public LocId? FailPopup; - - /// - /// The popup to show when access is denied due to wearing gloves. - /// - [DataField] - public LocId? FailGlovesPopup; } diff --git a/Content.Shared/FingerprintReader/FingerprintReaderSystem.cs b/Content.Shared/FingerprintReader/FingerprintReaderSystem.cs index aa7d190c34..73b06cac9b 100644 --- a/Content.Shared/FingerprintReader/FingerprintReaderSystem.cs +++ b/Content.Shared/FingerprintReader/FingerprintReaderSystem.cs @@ -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(OnFindAvailableLocks); + SubscribeLocalEvent(OnCheckLockAccess); + } + + private void OnFindAvailableLocks(Entity ent, ref FindAvailableLocksEvent args) + { + args.FoundReaders |= LockTypes.Fingerprint; + } + + private void OnCheckLockAccess(Entity 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; + } + /// /// Checks if the given user has fingerprint access to the target entity. /// /// The target entity. /// User trying to gain access. + /// Whether to display a popup with the reason you are not allowed to access this. + /// The reason why access was denied. /// True if access was granted, otherwise false. + // TODO: Remove showPopup, just keeping it here for backwards compatibility while I refactor mail [PublicAPI] - public bool IsAllowed(Entity target, EntityUid user, bool showPopup = true) + public bool IsAllowed(Entity 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(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; } diff --git a/Content.Shared/Lock/LockComponent.cs b/Content.Shared/Lock/LockComponent.cs index 1e5f0fdd50..822ec78f9b 100644 --- a/Content.Shared/Lock/LockComponent.cs +++ b/Content.Shared/Lock/LockComponent.cs @@ -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; /// - /// Whether the lock requires access validation through + /// Whether or not the lock is locked when used it hand. + /// + [DataField, AutoNetworkedField] + public bool LockInHand; + + /// + /// Whether or not the lock is unlocked when used in hand. + /// + [DataField, AutoNetworkedField] + public bool UnlockInHand; + + /// + /// Whether access requirements should be checked for this lock. /// [DataField, AutoNetworkedField] public bool UseAccess = true; + /// + /// What readers should be checked to determine if an entity has access. + /// If null, all possible readers are checked. + /// + [DataField, AutoNetworkedField] + public LockTypes? CheckedLocks; + + /// + /// Whether any reader needs to be accessed to operate this lock. + /// By default, all readers need to be able to be accessed. + /// + [DataField, AutoNetworkedField] + public bool CheckForAnyReaders; + /// /// The sound played when unlocked. /// @@ -96,6 +121,12 @@ public sealed partial class LockComponent : Component [DataField] [AutoNetworkedField] public TimeSpan UnlockTime; + + /// + /// Whether this lock can be locked again after being unlocked. + /// + [DataField, AutoNetworkedField] + public bool AllowRepeatedLocking = true; } /// diff --git a/Content.Shared/Lock/LockSystem.cs b/Content.Shared/Lock/LockSystem.cs index 2abb45d878..ca780780fb 100644 --- a/Content.Shared/Lock/LockSystem.cs +++ b/Content.Shared/Lock/LockSystem.cs @@ -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"; + /// public override void Initialize() { @@ -42,6 +43,7 @@ public sealed class LockSystem : EntitySystem SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnActivated, before: [typeof(ActivatableUISystem)]); + SubscribeLocalEvent(OnUseInHand, before: [typeof(ActivatableUISystem)]); SubscribeLocalEvent(OnStorageOpenAttempt); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent>(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. /// - public bool CanToggleLock(EntityUid uid, EntityUid user, bool quiet = true) + public bool CanToggleLock(Entity 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) + /// + /// Checks whether the user has access to locks on an entity. + /// + /// The entity we check for locks. + /// The user we check for access. + /// Whether to display a popup if user has no access. + /// True if the user has access, otherwise False. + [PublicAPI] + public bool HasUserAccess(Entity 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 } } } + +/// +/// Raised on an entity to check whether it has any readers that can prevent it from being opened. +/// +/// The person attempting to open the entity. +/// What readers were found. This should not be set when raising the event. +[ByRefEvent] +public record struct FindAvailableLocksEvent(EntityUid User, LockTypes FoundReaders = LockTypes.None); + +/// +/// Raised on an entity to check if the user has access (ID, Fingerprint, etc) to said entity. +/// +/// The user we are checking. +/// What readers we are attempting to verify access for. +/// Which readers the user has access to. This should not be set when raising the event. +[ByRefEvent] +public record struct CheckUserHasLockAccessEvent(EntityUid User, LockTypes FoundReaders = LockTypes.None, LockTypes HasAccess = LockTypes.None, string? DenyReason = null); + +/// +/// 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 will have the Access type, which is gathered by an event and handled by the respective system. +/// +[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, +} diff --git a/Resources/Prototypes/Entities/Objects/Deliveries/deliveries.yml b/Resources/Prototypes/Entities/Objects/Deliveries/deliveries.yml index 0c33f53881..a011460366 100644 --- a/Resources/Prototypes/Entities/Objects/Deliveries/deliveries.yml +++ b/Resources/Prototypes/Entities/Objects/Deliveries/deliveries.yml @@ -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