diff --git a/Content.Server/Access/Systems/AgentIDCardSystem.cs b/Content.Server/Access/Systems/AgentIDCardSystem.cs index 6385274336..0df760baef 100644 --- a/Content.Server/Access/Systems/AgentIDCardSystem.cs +++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs @@ -13,6 +13,7 @@ using Content.Server.Clothing.Systems; using Content.Server.Implants; using Content.Shared.Implants; using Content.Shared.Inventory; +using Content.Shared.Lock; using Content.Shared.PDA; namespace Content.Server.Access.Systems @@ -25,6 +26,7 @@ namespace Content.Server.Access.Systems [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ChameleonClothingSystem _chameleon = default!; [Dependency] private readonly ChameleonControllerSystem _chamController = default!; + [Dependency] private readonly LockSystem _lock = default!; public override void Initialize() { @@ -79,7 +81,8 @@ namespace Content.Server.Access.Systems private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args) { - if (args.Target == null || !args.CanReach || !TryComp(args.Target, out var targetAccess) || !HasComp(args.Target)) + if (args.Target == null || !args.CanReach || _lock.IsLocked(uid) || + !TryComp(args.Target, out var targetAccess) || !HasComp(args.Target)) return; if (!TryComp(uid, out var access) || !HasComp(uid)) diff --git a/Content.Server/VoiceMask/VoiceMaskSystem.cs b/Content.Server/VoiceMask/VoiceMaskSystem.cs index cd85ff2428..528acd58b0 100644 --- a/Content.Server/VoiceMask/VoiceMaskSystem.cs +++ b/Content.Server/VoiceMask/VoiceMaskSystem.cs @@ -5,11 +5,13 @@ using Content.Shared.Chat; using Content.Shared.Clothing; using Content.Shared.Database; using Content.Shared.Inventory; +using Content.Shared.Lock; using Content.Shared.Popups; using Content.Shared.Preferences; using Content.Shared.Speech; using Content.Shared.VoiceMask; using Robust.Shared.Configuration; +using Robust.Shared.Containers; using Robust.Shared.Prototypes; namespace Content.Server.VoiceMask; @@ -22,6 +24,8 @@ public sealed partial class VoiceMaskSystem : EntitySystem [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly LockSystem _lock = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; // CCVar. private int _maxNameLength; @@ -30,6 +34,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem { base.Initialize(); SubscribeLocalEvent>(OnTransformSpeakerName); + SubscribeLocalEvent(OnLockToggled); SubscribeLocalEvent(OnChangeName); SubscribeLocalEvent(OnChangeVerb); SubscribeLocalEvent(OnEquip); @@ -44,6 +49,14 @@ public sealed partial class VoiceMaskSystem : EntitySystem args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb; } + private void OnLockToggled(Entity ent, ref LockToggledEvent args) + { + if (args.Locked) + _actions.RemoveAction(ent.Comp.ActionEntity); + else if (_container.TryGetContainingContainer(ent.Owner, out var container)) + _actions.AddAction(container.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action, ent); + } + #region User inputs from UI private void OnChangeVerb(Entity entity, ref VoiceMaskChangeVerbMessage msg) { @@ -78,6 +91,9 @@ public sealed partial class VoiceMaskSystem : EntitySystem #region UI private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEquippedEvent args) { + if (_lock.IsLocked(uid)) + return; + _actions.AddAction(args.Wearer, ref component.ActionEntity, component.Action, uid); } diff --git a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs index 233a71acee..4b38d926f6 100644 --- a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Contraband; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; +using Content.Shared.Lock; using Content.Shared.Tag; using Content.Shared.Verbs; using Robust.Shared.Prototypes; @@ -23,6 +24,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly TagSystem _tag = default!; [Dependency] protected readonly IGameTiming _timing = default!; + [Dependency] private readonly LockSystem _lock = default!; private static readonly SlotFlags[] IgnoredSlots = { @@ -122,7 +124,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem private void OnVerb(Entity ent, ref GetVerbsEvent args) { - if (!args.CanAccess || !args.CanInteract || ent.Comp.User != args.User) + if (!args.CanAccess || !args.CanInteract || _lock.IsLocked(ent.Owner)) return; // Can't pass args from a ref event inside of lambdas diff --git a/Content.Shared/Item/ItemToggle/ItemToggleSystem.cs b/Content.Shared/Item/ItemToggle/ItemToggleSystem.cs index ff31faaaa1..367b078f23 100644 --- a/Content.Shared/Item/ItemToggle/ItemToggleSystem.cs +++ b/Content.Shared/Item/ItemToggle/ItemToggleSystem.cs @@ -78,7 +78,7 @@ public sealed class ItemToggleSystem : EntitySystem if (ent.Comp.Activated) { - var ev = new ItemToggleActivateAttemptEvent(args.User); + var ev = new ItemToggleDeactivateAttemptEvent(args.User); RaiseLocalEvent(ent.Owner, ref ev); if (ev.Cancelled) @@ -86,7 +86,7 @@ public sealed class ItemToggleSystem : EntitySystem } else { - var ev = new ItemToggleDeactivateAttemptEvent(args.User); + var ev = new ItemToggleActivateAttemptEvent(args.User); RaiseLocalEvent(ent.Owner, ref ev); if (ev.Cancelled) diff --git a/Content.Shared/Lock/ItemToggleRequiresLockComponent.cs b/Content.Shared/Lock/ItemToggleRequiresLockComponent.cs index 94b8729476..1f257a90c8 100644 --- a/Content.Shared/Lock/ItemToggleRequiresLockComponent.cs +++ b/Content.Shared/Lock/ItemToggleRequiresLockComponent.cs @@ -5,13 +5,19 @@ namespace Content.Shared.Lock; /// /// This is used for toggleable items that require the entity to have a lock in a certain state. /// -[RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(LockSystem))] public sealed partial class ItemToggleRequiresLockComponent : Component { /// /// TRUE: the lock must be locked to toggle the item. /// FALSE: the lock must be unlocked to toggle the item. /// - [DataField] + [DataField, AutoNetworkedField] public bool RequireLocked; + + /// + /// Popup text for when someone tries to toggle the item, but it's locked. If null, no popup will be shown. + /// + [DataField] + public LocId? LockedPopup = "lock-comp-generic-fail"; } diff --git a/Content.Shared/Lock/LockComponent.cs b/Content.Shared/Lock/LockComponent.cs index 0fdee2477f..1e5f0fdd50 100644 --- a/Content.Shared/Lock/LockComponent.cs +++ b/Content.Shared/Lock/LockComponent.cs @@ -21,6 +21,18 @@ public sealed partial class LockComponent : Component [AutoNetworkedField] public bool Locked = true; + /// + /// If true, will show verbs to lock and unlock the item. Otherwise, it will not. + /// + [DataField, AutoNetworkedField] + public bool ShowLockVerbs = true; + + /// + /// If true will show examine text. + /// + [DataField, AutoNetworkedField] + public bool ShowExamine = true; + /// /// Whether or not the lock is locked by simply clicking. /// @@ -44,7 +56,7 @@ public sealed partial class LockComponent : Component /// The sound played when unlocked. /// [DataField("unlockingSound"), ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier UnlockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg") + public SoundSpecifier? UnlockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg") { Params = AudioParams.Default.WithVolume(-5f), }; @@ -53,7 +65,7 @@ public sealed partial class LockComponent : Component /// The sound played when locked. /// [DataField("lockingSound"), ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier LockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_on.ogg") + public SoundSpecifier? LockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_on.ogg") { Params = AudioParams.Default.WithVolume(-5f) }; diff --git a/Content.Shared/Lock/LockSystem.cs b/Content.Shared/Lock/LockSystem.cs index 397a3636bb..6ca546f581 100644 --- a/Content.Shared/Lock/LockSystem.cs +++ b/Content.Shared/Lock/LockSystem.cs @@ -28,12 +28,12 @@ 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!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; /// public override void Initialize() @@ -54,8 +54,8 @@ public sealed class LockSystem : EntitySystem SubscribeLocalEvent(OnAttemptChangePanel); SubscribeLocalEvent(OnUnanchorAttempt); - SubscribeLocalEvent(OnUIOpenAttempt); - SubscribeLocalEvent(LockToggled); + SubscribeLocalEvent(OnUIOpenAttempt); + SubscribeLocalEvent(LockToggled); SubscribeLocalEvent(OnActivateAttempt); } @@ -96,6 +96,9 @@ public sealed class LockSystem : EntitySystem private void OnExamined(EntityUid uid, LockComponent lockComp, ExaminedEvent args) { + if (!lockComp.ShowExamine) + return; + args.PushText(Loc.GetString(lockComp.Locked ? "lock-comp-on-examined-is-locked" : "lock-comp-on-examined-is-unlocked", @@ -239,6 +242,20 @@ public sealed class LockSystem : EntitySystem return true; } + /// + /// Toggle the lock to locked if unlocked, and unlocked if locked. + /// + /// Entity to toggle the lock state of. + /// The person trying to toggle the lock + /// Entities lock comp (will be resolved) + public void ToggleLock(EntityUid uid, EntityUid? user, LockComponent? lockComp = null) + { + if (IsLocked((uid, lockComp))) + Unlock(uid, user, lockComp); + else + Lock(uid, user, lockComp); + } + /// /// Returns true if the entity is locked. /// Entities with no lock component are considered unlocked. @@ -287,7 +304,7 @@ public sealed class LockSystem : EntitySystem private void AddToggleLockVerb(EntityUid uid, LockComponent component, GetVerbsEvent args) { - if (!args.CanAccess || !args.CanInteract || !args.CanComplexInteract) + if (!args.CanAccess || !args.CanInteract || !args.CanComplexInteract || !component.ShowLockVerbs) return; AlternativeVerb verb = new() @@ -394,41 +411,54 @@ public sealed class LockSystem : EntitySystem args.Cancel(); } - private void OnUIOpenAttempt(EntityUid uid, ActivatableUIRequiresLockComponent component, ActivatableUIOpenAttemptEvent args) + private void OnUIOpenAttempt(EntityUid uid, UIRequiresLockComponent 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); - } + if (!TryComp(uid, out var lockComp) || lockComp.Locked == component.RequireLocked) + return; - _audio.PlayPredicted(component.AccessDeniedSound, uid, args.User); + args.Cancel(); + if (lockComp.Locked && component.Popup != null) + { + _sharedPopupSystem.PopupClient(Loc.GetString(component.Popup), uid, args.User); } + + _audio.PlayPredicted(component.AccessDeniedSound, uid, args.User); } - private void LockToggled(EntityUid uid, ActivatableUIRequiresLockComponent component, LockToggledEvent args) + private void LockToggled(EntityUid uid, UIRequiresLockComponent component, LockToggledEvent args) { if (!TryComp(uid, out var lockComp) || lockComp.Locked == component.RequireLocked) return; - _activatableUI.CloseAll(uid); + if (component.UserInterfaceKeys == null) + { + _ui.CloseUis(uid); + return; + } + + foreach (var key in component.UserInterfaceKeys) + { + _ui.CloseUi(uid, key); + } } + private void OnActivateAttempt(EntityUid uid, ItemToggleRequiresLockComponent component, ref ItemToggleActivateAttemptEvent args) { if (args.Cancelled) return; - if (TryComp(uid, out var lockComp) && lockComp.Locked != component.RequireLocked) + if (!TryComp(uid, out var lockComp) || lockComp.Locked == component.RequireLocked) + return; + + args.Cancelled = true; + + if (lockComp.Locked && component.LockedPopup != null) { - args.Cancelled = true; - if (lockComp.Locked) - _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-generic-fail", - ("target", Identity.Entity(uid, EntityManager))), + _sharedPopupSystem.PopupClient(Loc.GetString(component.LockedPopup, + ("target", Identity.Entity(uid, EntityManager))), uid, args.User); } diff --git a/Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs b/Content.Shared/Lock/UIRequiresLockComponent.cs similarity index 67% rename from Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs rename to Content.Shared/Lock/UIRequiresLockComponent.cs index a2a9d8c556..ab10526103 100644 --- a/Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs +++ b/Content.Shared/Lock/UIRequiresLockComponent.cs @@ -7,8 +7,15 @@ namespace Content.Shared.Lock; /// This is used for activatable UIs that require the entity to have a lock in a certain state. /// [RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))] -public sealed partial class ActivatableUIRequiresLockComponent : Component +public sealed partial class UIRequiresLockComponent : Component { + /// + /// UIs that are locked behind this component. + /// If null, will close all UIs. + /// + [DataField] + public List? UserInterfaceKeys; + /// /// TRUE: the lock must be locked to access the UI. /// FALSE: the lock must be unlocked to access the UI. @@ -21,4 +28,7 @@ public sealed partial class ActivatableUIRequiresLockComponent : Component /// [DataField] public SoundSpecifier? AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg"); + + [DataField] + public LocId? Popup = "entity-storage-component-locked-message"; } diff --git a/Content.Shared/SecretLocks/SharedVoiceTriggerLockSystem.cs b/Content.Shared/SecretLocks/SharedVoiceTriggerLockSystem.cs new file mode 100644 index 0000000000..483b3ec251 --- /dev/null +++ b/Content.Shared/SecretLocks/SharedVoiceTriggerLockSystem.cs @@ -0,0 +1,30 @@ +using Content.Shared.Item.ItemToggle; +using Content.Shared.Lock; +using Content.Shared.Trigger.Components.Triggers; + +namespace Content.Shared.SecretLocks; + +public sealed partial class SharedVoiceTriggerLockSystem : EntitySystem +{ + [Dependency] private readonly ItemToggleSystem _toggle = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnLockToggled); + } + + private void OnLockToggled(Entity ent, ref LockToggledEvent args) + { + if (!TryComp(ent.Owner, out var triggerComp)) + return; + + triggerComp.ShowVerbs = !args.Locked; + triggerComp.ShowExamine = !args.Locked; + + _toggle.TryDeactivate(ent.Owner, null, true, false); + + Dirty(ent.Owner, triggerComp); + } +} diff --git a/Content.Shared/SecretLocks/VoiceTriggerLockComponent.cs b/Content.Shared/SecretLocks/VoiceTriggerLockComponent.cs new file mode 100644 index 0000000000..345f76226f --- /dev/null +++ b/Content.Shared/SecretLocks/VoiceTriggerLockComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.SecretLocks; + +/// +/// "Locks" items (Doesn't actually lock them but just switches various settings) so its not possible to tell +/// the item is triggered by a voice activation. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class VoiceTriggerLockComponent : Component; diff --git a/Content.Shared/Trigger/Components/Effects/LockOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/LockOnTriggerComponent.cs new file mode 100644 index 0000000000..38eea0a461 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/LockOnTriggerComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Trigger.Components.Effects; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LockOnTriggerComponent : BaseXOnTriggerComponent +{ + [DataField, AutoNetworkedField] + public LockAction LockOnTrigger = LockAction.Toggle; +} + +[Serializable, NetSerializable] +public enum LockAction +{ + Lock = 0, + Unlock = 1, + Toggle = 2, +} diff --git a/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs b/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs index a36992d7da..1fc3c1b966 100644 --- a/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs +++ b/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs @@ -44,4 +44,52 @@ public sealed partial class TriggerOnVoiceComponent : BaseTriggerOnXComponent /// [DataField, AutoNetworkedField] public int MaxLength = 50; + + /// + /// When examining the item, should it show information about what word is recorded? + /// + [DataField, AutoNetworkedField] + public bool ShowExamine = true; + + /// + /// Should there be verbs that allow re-recording of the trigger word? + /// + [DataField, AutoNetworkedField] + public bool ShowVerbs = true; + + /// + /// The verb text that is shown when you can start recording a message. + /// + [DataField] + public LocId StartRecordingVerb = "trigger-on-voice-record"; + + /// + /// The verb text that is shown when you can stop recording a message. + /// + [DataField] + public LocId StopRecordingVerb = "trigger-on-voice-stop"; + + /// + /// Tooltip that appears when hovering over the stop or start recording verbs. + /// + [DataField] + public LocId? RecordingVerbMessage; + + /// + /// The verb text that is shown when you can clear a recording. + /// + [DataField] + public LocId ClearRecordingVerb = "trigger-on-voice-clear"; + + /// + /// The loc string that is shown when inspecting an uninitialized voice trigger. + /// + [DataField] + public LocId? InspectUninitializedLoc = "trigger-on-voice-uninitialized"; + + /// + /// The loc string to use when inspecting voice trigger. Will also include the triggering phrase + /// + [DataField] + public LocId? InspectInitializedLoc = "trigger-on-voice-examine"; } diff --git a/Content.Shared/Trigger/Systems/LockOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/LockOnTriggerSystem.cs new file mode 100644 index 0000000000..8726ede899 --- /dev/null +++ b/Content.Shared/Trigger/Systems/LockOnTriggerSystem.cs @@ -0,0 +1,35 @@ +using Content.Shared.Lock; +using Content.Shared.Trigger.Components.Effects; + +namespace Content.Shared.Trigger.Systems; + +public sealed class LockOnTriggerSystem : EntitySystem +{ + [Dependency] private readonly LockSystem _lock = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnTrigger); + } + + private void OnTrigger(Entity ent, ref TriggerEvent args) + { + if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key)) + return; + + switch (ent.Comp.LockOnTrigger) + { + case LockAction.Lock: + _lock.Lock(ent.Owner, args.User); + break; + case LockAction.Unlock: + _lock.Unlock(ent, args.User); + break; + case LockAction.Toggle: + _lock.ToggleLock(ent, args.User); + break; + } + } +} diff --git a/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs b/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs index ac67cb7ed2..c374369a7f 100644 --- a/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs +++ b/Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs @@ -25,15 +25,21 @@ public sealed partial class TriggerSystem RemCompDeferred(ent); } - private void OnVoiceExamine(Entity ent, ref ExaminedEvent args) + private void OnVoiceExamine(EntityUid uid, TriggerOnVoiceComponent component, ExaminedEvent args) { - if (args.IsInDetailsRange) + if (!args.IsInDetailsRange || !component.ShowExamine) + return; + + if (component.InspectUninitializedLoc != null && string.IsNullOrWhiteSpace(component.KeyPhrase)) { - args.PushText(string.IsNullOrWhiteSpace(ent.Comp.KeyPhrase) - ? Loc.GetString("trigger-on-voice-uninitialized") - : Loc.GetString("trigger-on-voice-examine", ("keyphrase", ent.Comp.KeyPhrase))); + args.PushText(Loc.GetString(component.InspectUninitializedLoc)); + } + else if (component.InspectInitializedLoc != null && !string.IsNullOrWhiteSpace(component.KeyPhrase)) + { + args.PushText(Loc.GetString(component.InspectInitializedLoc.Value, ("keyphrase", component.KeyPhrase))); } } + private void OnListen(Entity ent, ref ListenEvent args) { var component = ent.Comp; @@ -71,13 +77,13 @@ public sealed partial class TriggerSystem private void OnVoiceGetAltVerbs(Entity ent, ref GetVerbsEvent args) { - if (!args.CanInteract || !args.CanAccess) + if (!args.CanInteract || !args.CanAccess || !ent.Comp.ShowVerbs) return; var user = args.User; args.Verbs.Add(new AlternativeVerb { - Text = Loc.GetString(ent.Comp.IsRecording ? "trigger-on-voice-stop" : "trigger-on-voice-record"), + Text = Loc.GetString(ent.Comp.IsRecording ? ent.Comp.StopRecordingVerb : ent.Comp.StartRecordingVerb), Act = () => { if (ent.Comp.IsRecording) @@ -93,7 +99,7 @@ public sealed partial class TriggerSystem args.Verbs.Add(new AlternativeVerb { - Text = Loc.GetString("trigger-on-voice-clear"), + Text = Loc.GetString(ent.Comp.ClearRecordingVerb), Act = () => { ClearRecording(ent); diff --git a/Content.Shared/UserInterface/ActivatableUISystem.cs b/Content.Shared/UserInterface/ActivatableUISystem.cs index a3f9a033df..d1df375ccb 100644 --- a/Content.Shared/UserInterface/ActivatableUISystem.cs +++ b/Content.Shared/UserInterface/ActivatableUISystem.cs @@ -116,7 +116,7 @@ public sealed partial class ActivatableUISystem : EntitySystem } } - return args.CanInteract || HasComp(args.User) && !component.BlockSpectators; + return (args.CanInteract || HasComp(args.User) && !component.BlockSpectators) && !RaiseCanOpenEventChecks(args.User, uid); } private void OnUseInHand(EntityUid uid, ActivatableUIComponent component, UseInHandEvent args) @@ -225,11 +225,7 @@ public sealed partial class ActivatableUISystem : EntitySystem // If we've gotten this far, fire a cancellable event that indicates someone is about to activate this. // This is so that stuff can require further conditions (like power). - var oae = new ActivatableUIOpenAttemptEvent(user); - var uae = new UserOpenActivatableUIAttemptEvent(user, uiEntity); - RaiseLocalEvent(user, uae); - RaiseLocalEvent(uiEntity, oae); - if (oae.Cancelled || uae.Cancelled) + if (RaiseCanOpenEventChecks(user, uiEntity)) return false; // Give the UI an opportunity to prepare itself if it needs to do anything @@ -286,4 +282,15 @@ public sealed partial class ActivatableUISystem : EntitySystem if (ent.Comp.InHandsOnly) CloseAll(ent, ent); } + + private bool RaiseCanOpenEventChecks(EntityUid user, EntityUid uiEntity) + { + // If we've gotten this far, fire a cancellable event that indicates someone is about to activate this. + // This is so that stuff can require further conditions (like power). + var oae = new ActivatableUIOpenAttemptEvent(user); + var uae = new UserOpenActivatableUIAttemptEvent(user, uiEntity); + RaiseLocalEvent(user, uae); + RaiseLocalEvent(uiEntity, oae); + return oae.Cancelled || uae.Cancelled; + } } diff --git a/Resources/Locale/en-US/access/components/agent-id-card-component.ftl b/Resources/Locale/en-US/access/components/agent-id-card-component.ftl index 5e1e3cd7cf..c645967d98 100644 --- a/Resources/Locale/en-US/access/components/agent-id-card-component.ftl +++ b/Resources/Locale/en-US/access/components/agent-id-card-component.ftl @@ -8,3 +8,5 @@ agent-id-card-current-name = Name: agent-id-card-current-job = Job: agent-id-card-job-icon-label = Job icon: agent-id-menu-title = Agent ID Card + +agent-id-open-ui-verb = Change settings diff --git a/Resources/Locale/en-US/locks/voice-trigger-lock.ftl b/Resources/Locale/en-US/locks/voice-trigger-lock.ftl new file mode 100644 index 0000000000..fd2dc38d23 --- /dev/null +++ b/Resources/Locale/en-US/locks/voice-trigger-lock.ftl @@ -0,0 +1,5 @@ +voice-trigger-lock-verb-record = Record lock phrase +voice-trigger-lock-verb-message = Locking the item will disable features that reveal its true nature! + +voice-trigger-lock-on-uninitialized = The display is blank +voice-trigger-lock-on-examine = The display shows the passphrase: "{$keyphrase}" diff --git a/Resources/Prototypes/Entities/Clothing/Back/specific.yml b/Resources/Prototypes/Entities/Clothing/Back/specific.yml index 005dfedba5..b80e7709d5 100644 --- a/Resources/Prototypes/Entities/Clothing/Back/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Back/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingBackpack + parent: [ClothingBackpack, BaseChameleon] id: ClothingBackpackChameleon name: backpack description: You wear this on your back and put items into it. diff --git a/Resources/Prototypes/Entities/Clothing/Ears/specific.yml b/Resources/Prototypes/Entities/Clothing/Ears/specific.yml index 83612ac4b2..987c3f7340 100644 --- a/Resources/Prototypes/Entities/Clothing/Ears/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Ears/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingHeadsetGrey + parent: [ClothingHeadsetGrey, BaseChameleon] id: ClothingHeadsetChameleon name: passenger headset description: An updated, modular intercom that fits over the head. Takes encryption keys. @@ -14,7 +14,3 @@ - type: ChameleonClothing slot: [ears] default: ClothingHeadsetGrey - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/specific.yml b/Resources/Prototypes/Entities/Clothing/Eyes/specific.yml index 936856fdf7..b62773fe50 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingEyesBase + parent: [ClothingEyesBase, BaseChameleon] id: ClothingEyesChameleon # no flash immunity, sorry name: sun glasses description: Useful both for security and cargonia. @@ -7,7 +7,7 @@ components: - type: Tag tags: # intentionally no WhitelistChameleon tag - - PetWearable + - PetWearable - type: Sprite sprite: Clothing/Eyes/Glasses/sunglasses.rsi - type: Clothing @@ -15,8 +15,3 @@ - type: ChameleonClothing slot: [eyes] default: ClothingEyesGlassesSunglasses - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface - diff --git a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml index 6140bcd8ed..1a2ed4301a 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingHandsButcherable + parent: [ClothingHandsButcherable, BaseChameleon] id: ClothingHandsChameleon # doesn't protect from electricity or heat name: black gloves description: Regular black gloves that do not keep you from frying. @@ -17,10 +17,6 @@ - type: Fiber fiberMaterial: fibers-chameleon - type: FingerprintMask - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface - type: entity parent: ClothingHandsChameleon diff --git a/Resources/Prototypes/Entities/Clothing/Head/specific.yml b/Resources/Prototypes/Entities/Clothing/Head/specific.yml index 15f89da106..4539e2fdf3 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingHeadBase + parent: [ClothingHeadBase, BaseChameleon] id: ClothingHeadHatChameleon name: beret description: A beret, an artists favorite headwear. @@ -14,10 +14,6 @@ - type: ChameleonClothing slot: [HEAD] default: ClothingHeadHatBeret - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface - type: entity parent: ClothingHeadHatFedoraBrown diff --git a/Resources/Prototypes/Entities/Clothing/Masks/specific.yml b/Resources/Prototypes/Entities/Clothing/Masks/specific.yml index 286dfd6cf1..33f0ec3ad6 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingMaskBase + parent: [ClothingMaskBase, BaseChameleon] id: ClothingMaskGasChameleon name: gas mask description: A face-covering mask that can be connected to an air supply. @@ -16,10 +16,6 @@ default: ClothingMaskGas - type: BreathMask - type: IdentityBlocker # need that for default ClothingMaskGas - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface - type: HideLayerClothing slots: - Snout @@ -30,6 +26,12 @@ suffix: Voice Mask, Chameleon components: - type: VoiceMask + - type: UIRequiresLock + userInterfaceKeys: + - enum.ChameleonUiKey.Key + - enum.VoiceMaskUIKey.Key + accessDeniedSound: null + popup: null - type: HideLayerClothing slots: - Snout diff --git a/Resources/Prototypes/Entities/Clothing/Neck/specific.yml b/Resources/Prototypes/Entities/Clothing/Neck/specific.yml index b98cdd02e0..a32d9b5eea 100644 --- a/Resources/Prototypes/Entities/Clothing/Neck/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Neck/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingNeckBase + parent: [ClothingNeckBase, BaseChameleon] id: ClothingNeckChameleon name: striped red scarf description: A stylish striped red scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks. @@ -14,7 +14,3 @@ - type: ChameleonClothing slot: [NECK] default: ClothingNeckScarfStripedRed - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/specific.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/specific.yml index aec34e80ca..8b2116f0ca 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingOuterBase + parent: [ClothingOuterBase, BaseChameleon] id: ClothingOuterChameleon name: vest description: A thick vest with a rubbery, water-resistant shell. @@ -14,10 +14,6 @@ - type: ChameleonClothing slot: [outerClothing] default: ClothingOuterVest - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface - type: TemperatureProtection # Same as a basic winter coat. heatingCoefficient: 1.1 coolingCoefficient: 0.1 diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml index ebf304557e..cfdc967a7e 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml @@ -163,7 +163,7 @@ sprite: Clothing/Shoes/Specific/wizard.rsi - type: entity - parent: ClothingShoesBase + parent: [ClothingShoesBase, BaseChameleon] id: ClothingShoesChameleon name: black shoes suffix: Chameleon @@ -197,10 +197,6 @@ - type: ChameleonClothing slot: [FEET] default: ClothingShoesColorBlack - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface - type: entity parent: ClothingShoesChameleon diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/specific.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/specific.yml index a2c903bac3..f7ce37fe19 100644 --- a/Resources/Prototypes/Entities/Clothing/Uniforms/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Uniforms/specific.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingUniformBase + parent: [ClothingUniformBase, BaseChameleon] id: ClothingUniformJumpsuitChameleon name: black jumpsuit description: A generic black jumpsuit with no rank markings. @@ -36,7 +36,3 @@ - type: ChameleonClothing slot: [innerclothing] default: ClothingUniformJumpsuitColorBlack - - type: UserInterface - interfaces: - enum.ChameleonUiKey.Key: - type: ChameleonBoundUserInterface diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index cb95c231e9..34fdbbd74b 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -167,7 +167,9 @@ - type: Lock locked: true unlockOnClick: false - - type: ActivatableUIRequiresLock + - type: UIRequiresLock + userInterfaceKeys: + - enum.BorgUiKey.Key - type: LockedWiresPanel - type: Damageable damageContainer: Silicon diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index d9b2b83993..4c6facdd4f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2320,6 +2320,7 @@ types: Heat : 0.2 #per second, scales with temperature & other constants +# TODO: Make grenade penguin voice activated like the rest of the stealth items. - type: entity name: grenade penguin parent: [ MobPenguin, MobCombat, BaseSyndicateContraband ] diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 2082d9a152..92bd4297ad 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -1467,7 +1467,7 @@ - MedTekCartridge - type: entity - parent: BasePDA + parent: [BasePDA, VoiceLock] id: ChameleonPDA name: passenger PDA description: Why isn't it gray? diff --git a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml index c4c5cc0aaf..d236383226 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml @@ -580,7 +580,7 @@ - type: entity name: passenger ID card - parent: IDCardStandard + parent: [IDCardStandard, BaseChameleon] id: AgentIDCard suffix: Agent components: @@ -595,9 +595,11 @@ - state: default - state: idpassenger - type: AgentIDCard + - type: UIRequiresLock - type: ActivatableUI key: enum.AgentIDCardUiKey.Key inHandsOnly: true + verbText: agent-id-open-ui-verb - type: Tag tags: - DoorBumpOpener diff --git a/Resources/Prototypes/Entities/Objects/Specific/locks.yml b/Resources/Prototypes/Entities/Objects/Specific/locks.yml new file mode 100644 index 0000000000..296b9fd7d9 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Specific/locks.yml @@ -0,0 +1,24 @@ +- type: entity + id: VoiceLock + abstract: true + components: + - type: Lock + locked: false + showLockVerbs: false + showExamine: false + lockOnClick: false + unlockOnClick: false + useAccess: false + unlockingSound: null # TODO: Maybe add sounds but just to the user? + lockingSound: null + lockTime: 0 + unlockTime: 0 + - type: TriggerOnVoice + listenRange: 2 # more fun + startRecordingVerb: voice-trigger-lock-verb-record + recordingVerbMessage: voice-trigger-lock-verb-message + inspectUninitializedLoc: voice-trigger-lock-on-uninitialized + inspectInitializedLoc: voice-trigger-lock-on-examine + - type: LockOnTrigger + - type: ActiveListener + - type: VoiceTriggerLock diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml index cbf437d0b2..2847b723d8 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml @@ -53,7 +53,7 @@ - type: DisarmMalus - type: entity - parent: Cane + parent: [Cane, VoiceLock] id: CaneSheath suffix: Empty components: @@ -69,6 +69,9 @@ interfaces: enum.StorageUiKey.Key: type: StorageBoundUserInterface + - type: ItemSlotsLock + slots: + - item - type: ItemSlots slots: item: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml index f879e2891e..f634425006 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml @@ -159,7 +159,7 @@ - type: entity name: pen - parent: BaseMeleeWeaponEnergy + parent: [BaseMeleeWeaponEnergy, VoiceLock] id: EnergyDagger suffix: E-Dagger description: 'A dark ink pen.' @@ -222,6 +222,16 @@ damage: types: Blunt: 1 + - type: EmitSoundOnUse + sound: + path: /Audio/Items/pen_click.ogg + params: + volume: -4 + maxDistance: 2 + - type: UseDelay + delay: 1.5 + - type: ItemToggleRequiresLock # TODO: FIX THIS VERB IS COOKED + lockedPopup: null - type: Tag tags: - Write diff --git a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml index 0662094143..2ab12c1705 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml @@ -37,7 +37,9 @@ 3: { state: can-o3, shader: "unshaded" } - type: ActivatableUI key: enum.GasCanisterUiKey.Key - - type: ActivatableUIRequiresLock + - type: UIRequiresLock + userInterfaceKeys: + - enum.GasCanisterUiKey.Key - type: UserInterface interfaces: enum.GasCanisterUiKey.Key: diff --git a/Resources/Prototypes/chameleon.yml b/Resources/Prototypes/chameleon.yml new file mode 100644 index 0000000000..6969380621 --- /dev/null +++ b/Resources/Prototypes/chameleon.yml @@ -0,0 +1,15 @@ +# for clothing that can be toggled, like magboots +- type: entity + parent: VoiceLock + abstract: true + id: BaseChameleon + components: + - type: UIRequiresLock + userInterfaceKeys: + - enum.ChameleonUiKey.Key + accessDeniedSound: null + popup: null + - type: UserInterface + interfaces: + enum.ChameleonUiKey.Key: + type: ChameleonBoundUserInterface