using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.ActionBlocker; using Content.Shared.Construction.Components; using Content.Shared.DoAfter; using Content.Shared.Emag.Systems; using Content.Shared.Examine; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Storage; using Content.Shared.Storage.Components; using Content.Shared.UserInterface; using Content.Shared.Verbs; using Content.Shared.Wires; using JetBrains.Annotations; using Robust.Shared.Audio.Systems; using Robust.Shared.Utility; namespace Content.Shared.Lock; /// /// Handles (un)locking and examining of Lock components /// [UsedImplicitly] public sealed class LockSystem : EntitySystem { [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly ActivatableUISystem _activatableUI = default!; [Dependency] private readonly EmagSystem _emag = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _sharedPopupSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; /// public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnActivated, before: [typeof(ActivatableUISystem)]); SubscribeLocalEvent(OnStorageOpenAttempt); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent>(AddToggleLockVerb); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnDoAfterLock); SubscribeLocalEvent(OnDoAfterUnlock); SubscribeLocalEvent(OnStorageInteractAttempt); SubscribeLocalEvent(OnLockToggleAttempt); SubscribeLocalEvent(OnAttemptChangePanel); SubscribeLocalEvent(OnUnanchorAttempt); SubscribeLocalEvent(OnUIOpenAttempt); SubscribeLocalEvent(LockToggled); } private void OnStartup(EntityUid uid, LockComponent lockComp, ComponentStartup args) { _appearanceSystem.SetData(uid, LockVisuals.Locked, lockComp.Locked); } private void OnActivated(EntityUid uid, LockComponent lockComp, ActivateInWorldEvent args) { if (args.Handled || !args.Complex) return; // Only attempt an unlock by default on Activate if (lockComp.Locked && lockComp.UnlockOnClick) { args.Handled = TryUnlock(uid, args.User, lockComp); } else if (!lockComp.Locked && lockComp.LockOnClick) { args.Handled = TryLock(uid, args.User, lockComp); } } private void OnStorageOpenAttempt(EntityUid uid, LockComponent component, ref StorageOpenAttemptEvent args) { if (!component.Locked) return; if (!args.Silent) _sharedPopupSystem.PopupClient(Loc.GetString("entity-storage-component-locked-message"), uid, args.User); args.Cancelled = true; } private void OnExamined(EntityUid uid, LockComponent lockComp, ExaminedEvent args) { args.PushText(Loc.GetString(lockComp.Locked ? "lock-comp-on-examined-is-locked" : "lock-comp-on-examined-is-unlocked", ("entityName", Identity.Name(uid, EntityManager)))); } /// /// Attmempts to lock a given entity /// /// /// If the lock is set to require a do-after, a true return value only indicates that the do-after started. /// /// The entity with the lock /// The person trying to lock it /// /// If true, skip the required do-after if one is configured. /// If locking was successful public bool TryLock(EntityUid uid, EntityUid user, LockComponent? lockComp = null, bool skipDoAfter = false) { if (!Resolve(uid, ref lockComp)) return false; if (!CanToggleLock(uid, user, quiet: false)) return false; if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false)) return false; if (!skipDoAfter && lockComp.LockTime != TimeSpan.Zero) { return _doAfter.TryStartDoAfter( new DoAfterArgs(EntityManager, user, lockComp.LockTime, new LockDoAfter(), uid, uid) { BreakOnDamage = true, BreakOnMove = true, NeedHand = true, BreakOnDropItem = false, }); } Lock(uid, user, lockComp); return true; } /// /// Forces a given entity to be locked, does not activate a do-after. /// public void Lock(EntityUid uid, EntityUid? user, LockComponent? lockComp = null) { if (!Resolve(uid, ref lockComp)) return; if (lockComp.Locked) return; if (user is { Valid: true }) { _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-lock-success", ("entityName", Identity.Name(uid, EntityManager))), uid, user); } _audio.PlayPredicted(lockComp.LockSound, uid, user); lockComp.Locked = true; _appearanceSystem.SetData(uid, LockVisuals.Locked, true); Dirty(uid, lockComp); var ev = new LockToggledEvent(true); RaiseLocalEvent(uid, ref ev, true); } /// /// Forces a given entity to be unlocked /// /// /// This does not process do-after times. /// /// The entity with the lock /// The person unlocking it. Can be null /// public void Unlock(EntityUid uid, EntityUid? user, LockComponent? lockComp = null) { if (!Resolve(uid, ref lockComp)) return; if (!lockComp.Locked) return; if (user is { Valid: true }) { _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-unlock-success", ("entityName", Identity.Name(uid, EntityManager))), uid, user.Value); } _audio.PlayPredicted(lockComp.UnlockSound, uid, user); lockComp.Locked = false; _appearanceSystem.SetData(uid, LockVisuals.Locked, false); Dirty(uid, lockComp); var ev = new LockToggledEvent(false); RaiseLocalEvent(uid, ref ev, true); } /// /// Attmempts to unlock a given entity /// /// /// If the lock is set to require a do-after, a true return value only indicates that the do-after started. /// /// The entity with the lock /// The person trying to unlock it /// /// If true, skip the required do-after if one is configured. /// If locking was successful public bool TryUnlock(EntityUid uid, EntityUid user, LockComponent? lockComp = null, bool skipDoAfter = false) { if (!Resolve(uid, ref lockComp)) return false; if (!CanToggleLock(uid, user, quiet: false)) return false; if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false)) return false; if (!skipDoAfter && lockComp.UnlockTime != TimeSpan.Zero) { return _doAfter.TryStartDoAfter( new DoAfterArgs(EntityManager, user, lockComp.LockTime, new UnlockDoAfter(), uid, uid) { BreakOnDamage = true, BreakOnMove = true, NeedHand = true, BreakOnDropItem = false, }); } Unlock(uid, user, lockComp); return true; } /// /// Returns true if the entity is locked. /// Entities with no lock component are considered unlocked. /// public bool IsLocked(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return false; return ent.Comp.Locked; } /// /// 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) { if (!_actionBlocker.CanComplexInteract(user)) return false; var ev = new LockToggleAttemptEvent(user, quiet); RaiseLocalEvent(uid, ref ev, true); if (ev.Cancelled) return false; var userEv = new UserLockToggleAttemptEvent(uid, 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) { // Not having an AccessComponent means you get free access. woo! if (!Resolve(uid, ref reader, false)) return true; if (_accessReader.IsAllowed(user, uid, reader)) return true; if (!quiet) _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-has-user-access-fail"), uid, user); return false; } private void AddToggleLockVerb(EntityUid uid, LockComponent component, GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract) return; AlternativeVerb verb = new() { Act = component.Locked ? () => TryUnlock(uid, args.User, component) : () => TryLock(uid, args.User, component), Text = Loc.GetString(component.Locked ? "toggle-lock-verb-unlock" : "toggle-lock-verb-lock"), Icon = !component.Locked ? new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/lock.svg.192dpi.png")) : new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unlock.svg.192dpi.png")), }; args.Verbs.Add(verb); } private void OnEmagged(EntityUid uid, LockComponent component, ref GotEmaggedEvent args) { if (!_emag.CompareFlag(args.Type, EmagType.Access)) return; if (!component.Locked || !component.BreakOnAccessBreaker) return; _audio.PlayPredicted(component.UnlockSound, uid, args.UserUid); component.Locked = false; _appearanceSystem.SetData(uid, LockVisuals.Locked, false); Dirty(uid, component); var ev = new LockToggledEvent(false); RaiseLocalEvent(uid, ref ev, true); args.Repeatable = true; args.Handled = true; } private void OnDoAfterLock(EntityUid uid, LockComponent component, LockDoAfter args) { if (args.Cancelled) return; TryLock(uid, args.User, skipDoAfter: true); } private void OnDoAfterUnlock(EntityUid uid, LockComponent component, UnlockDoAfter args) { if (args.Cancelled) return; TryUnlock(uid, args.User, skipDoAfter: true); } private void OnStorageInteractAttempt(Entity ent, ref StorageInteractAttemptEvent args) { if (ent.Comp.Locked) args.Cancelled = true; } private void OnLockToggleAttempt(Entity ent, ref LockToggleAttemptEvent args) { if (args.Cancelled) return; if (!TryComp(ent, out var panel) || !panel.Open) return; if (!args.Silent) { _sharedPopupSystem.PopupClient(Loc.GetString("construction-step-condition-wire-panel-close"), ent, args.User); } args.Cancelled = true; } private void OnAttemptChangePanel(Entity ent, ref AttemptChangePanelEvent args) { if (args.Cancelled) return; if (!TryComp(ent, out var lockComp) || !lockComp.Locked) return; _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-generic-fail", ("target", Identity.Entity(ent, EntityManager))), ent, args.User); args.Cancelled = true; } private void OnUnanchorAttempt(Entity ent, ref UnanchorAttemptEvent args) { if (args.Cancelled) return; if (!TryComp(ent, out var lockComp) || !lockComp.Locked) return; _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-generic-fail", ("target", Identity.Entity(ent, EntityManager))), ent, args.User); args.Cancel(); } private void OnUIOpenAttempt(EntityUid uid, ActivatableUIRequiresLockComponent component, ActivatableUIOpenAttemptEvent args) { if (args.Cancelled) return; if (TryComp(uid, out var lockComp) && lockComp.Locked != component.RequireLocked) { args.Cancel(); if (lockComp.Locked) { _sharedPopupSystem.PopupClient(Loc.GetString("entity-storage-component-locked-message"), uid, args.User); } _audio.PlayPredicted(component.AccessDeniedSound, uid, args.User); } } private void LockToggled(EntityUid uid, ActivatableUIRequiresLockComponent component, LockToggledEvent args) { if (!TryComp(uid, out var lockComp) || lockComp.Locked == component.RequireLocked) return; _activatableUI.CloseAll(uid); } }