]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add voice locks to various hidden syndicate items (#39310)
authorbeck-thompson <107373427+beck-thompson@users.noreply.github.com>
Sun, 10 Aug 2025 18:10:13 +0000 (11:10 -0700)
committerGitHub <noreply@github.com>
Sun, 10 Aug 2025 18:10:13 +0000 (11:10 -0700)
36 files changed:
Content.Server/Access/Systems/AgentIDCardSystem.cs
Content.Server/VoiceMask/VoiceMaskSystem.cs
Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs
Content.Shared/Item/ItemToggle/ItemToggleSystem.cs
Content.Shared/Lock/ItemToggleRequiresLockComponent.cs
Content.Shared/Lock/LockComponent.cs
Content.Shared/Lock/LockSystem.cs
Content.Shared/Lock/UIRequiresLockComponent.cs [moved from Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs with 67% similarity]
Content.Shared/SecretLocks/SharedVoiceTriggerLockSystem.cs [new file with mode: 0644]
Content.Shared/SecretLocks/VoiceTriggerLockComponent.cs [new file with mode: 0644]
Content.Shared/Trigger/Components/Effects/LockOnTriggerComponent.cs [new file with mode: 0644]
Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs
Content.Shared/Trigger/Systems/LockOnTriggerSystem.cs [new file with mode: 0644]
Content.Shared/Trigger/Systems/TriggerSystem.Voice.cs
Content.Shared/UserInterface/ActivatableUISystem.cs
Resources/Locale/en-US/access/components/agent-id-card-component.ftl
Resources/Locale/en-US/locks/voice-trigger-lock.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Clothing/Back/specific.yml
Resources/Prototypes/Entities/Clothing/Ears/specific.yml
Resources/Prototypes/Entities/Clothing/Eyes/specific.yml
Resources/Prototypes/Entities/Clothing/Hands/specific.yml
Resources/Prototypes/Entities/Clothing/Head/specific.yml
Resources/Prototypes/Entities/Clothing/Masks/specific.yml
Resources/Prototypes/Entities/Clothing/Neck/specific.yml
Resources/Prototypes/Entities/Clothing/OuterClothing/specific.yml
Resources/Prototypes/Entities/Clothing/Shoes/specific.yml
Resources/Prototypes/Entities/Clothing/Uniforms/specific.yml
Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/Entities/Objects/Devices/pda.yml
Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
Resources/Prototypes/Entities/Objects/Specific/locks.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml
Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
Resources/Prototypes/chameleon.yml [new file with mode: 0644]

index 6385274336a1a29db0314082f670efa240b8c48a..0df760baef1de26882274ea85a65c679c0b87570 100644 (file)
@@ -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<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
+            if (args.Target == null || !args.CanReach || _lock.IsLocked(uid) ||
+                !TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
                 return;
 
             if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))
index cd85ff242847ce18009bd13ea688de4b0fb5bfc0..528acd58b0ad335e9a2fc368cd1357d809893d31 100644 (file)
@@ -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<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName);
+        SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
         SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName);
         SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb);
         SubscribeLocalEvent<VoiceMaskComponent, ClothingGotEquippedEvent>(OnEquip);
@@ -44,6 +49,14 @@ public sealed partial class VoiceMaskSystem : EntitySystem
         args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb;
     }
 
+    private void OnLockToggled(Entity<VoiceMaskComponent> 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<VoiceMaskComponent> 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);
     }
 
index 233a71acee7875acc045b55450edd2f38c66f8a6..4b38d926f6d82bc74a6c77c3f598ed1c376408a5 100644 (file)
@@ -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<ChameleonClothingComponent> ent, ref GetVerbsEvent<InteractionVerb> 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
index ff31faaaa1a097fbe1e6298e709718542eccb2e5..367b078f23147fc8893d2d83ab2ed5ac1abec693 100644 (file)
@@ -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)
index 94b87294766da0577b60cc44019e2e654752f33e..1f257a90c8a798728376f18cd19936462e48a6e5 100644 (file)
@@ -5,13 +5,19 @@ namespace Content.Shared.Lock;
 /// <summary>
 /// This is used for toggleable items that require the entity to have a lock in a certain state.
 /// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(LockSystem))]
 public sealed partial class ItemToggleRequiresLockComponent : Component
 {
     /// <summary>
     /// TRUE: the lock must be locked to toggle the item.
     /// FALSE: the lock must be unlocked to toggle the item.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool RequireLocked;
+
+    /// <summary>
+    /// Popup text for when someone tries to toggle the item, but it's locked. If null, no popup will be shown.
+    /// </summary>
+    [DataField]
+    public LocId? LockedPopup = "lock-comp-generic-fail";
 }
index 0fdee2477f3d4b7df96d1d41d6ac3b7a8d9e6b1b..1e5f0fdd50d199c811243a96541ef539e309b9f0 100644 (file)
@@ -21,6 +21,18 @@ public sealed partial class LockComponent : Component
     [AutoNetworkedField]
     public bool Locked  = true;
 
+    /// <summary>
+    /// If true, will show verbs to lock and unlock the item. Otherwise, it will not.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool ShowLockVerbs = true;
+
+    /// <summary>
+    /// If true will show examine text.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool ShowExamine = true;
+
     /// <summary>
     /// Whether or not the lock is locked by simply clicking.
     /// </summary>
@@ -44,7 +56,7 @@ public sealed partial class LockComponent : Component
     /// The sound played when unlocked.
     /// </summary>
     [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.
     /// </summary>
     [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)
     };
index 397a3636bb0dbce67aae9ef4b2fb6fdcc859a171..6ca546f5819c065c06111a908e143c078b3b3a9d 100644 (file)
@@ -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!;
 
     /// <inheritdoc />
     public override void Initialize()
@@ -54,8 +54,8 @@ public sealed class LockSystem : EntitySystem
         SubscribeLocalEvent<LockedWiresPanelComponent, AttemptChangePanelEvent>(OnAttemptChangePanel);
         SubscribeLocalEvent<LockedAnchorableComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
 
-        SubscribeLocalEvent<ActivatableUIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
-        SubscribeLocalEvent<ActivatableUIRequiresLockComponent, LockToggledEvent>(LockToggled);
+        SubscribeLocalEvent<UIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
+        SubscribeLocalEvent<UIRequiresLockComponent, LockToggledEvent>(LockToggled);
 
         SubscribeLocalEvent<ItemToggleRequiresLockComponent, ItemToggleActivateAttemptEvent>(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;
     }
 
+    /// <summary>
+    /// Toggle the lock to locked if unlocked, and unlocked if locked.
+    /// </summary>
+    /// <param name="uid">Entity to toggle the lock state of.</param>
+    /// <param name="user">The person trying to toggle the lock</param>
+    /// <param name="lockComp">Entities lock comp (will be resolved)</param>
+    public void ToggleLock(EntityUid uid, EntityUid? user, LockComponent? lockComp = null)
+    {
+        if (IsLocked((uid, lockComp)))
+            Unlock(uid, user, lockComp);
+        else
+            Lock(uid, user, lockComp);
+    }
+
     /// <summary>
     /// 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<AlternativeVerb> 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<LockComponent>(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<LockComponent>(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<LockComponent>(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<LockComponent>(uid, out var lockComp) && lockComp.Locked != component.RequireLocked)
+        if (!TryComp<LockComponent>(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);
         }
similarity index 67%
rename from Content.Shared/Lock/ActivatableUIRequiresLockComponent.cs
rename to Content.Shared/Lock/UIRequiresLockComponent.cs
index a2a9d8c556f171661fd89873817ce4e2522f70c7..ab10526103ba3aeac9fbda1da34360e547133e2a 100644 (file)
@@ -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.
 /// </summary>
 [RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))]
-public sealed partial class ActivatableUIRequiresLockComponent : Component
+public sealed partial class UIRequiresLockComponent : Component
 {
+    /// <summary>
+    /// UIs that are locked behind this component.
+    /// If null, will close all UIs.
+    /// </summary>
+    [DataField]
+    public List<Enum>? UserInterfaceKeys;
+
     /// <summary>
     /// 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
     /// </summary>
     [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 (file)
index 0000000..483b3ec
--- /dev/null
@@ -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<VoiceTriggerLockComponent, LockToggledEvent>(OnLockToggled);
+    }
+
+    private void OnLockToggled(Entity<VoiceTriggerLockComponent> ent, ref LockToggledEvent args)
+    {
+        if (!TryComp<TriggerOnVoiceComponent>(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 (file)
index 0000000..345f762
--- /dev/null
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.SecretLocks;
+
+/// <summary>
+/// "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.
+/// </summary>
+[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 (file)
index 0000000..38eea0a
--- /dev/null
@@ -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,
+}
index a36992d7da8d408df957e32a145b2fcfa1a24efa..1fc3c1b966ce6859be18328982085367cac3b500 100644 (file)
@@ -44,4 +44,52 @@ public sealed partial class TriggerOnVoiceComponent : BaseTriggerOnXComponent
     /// </summary>
     [DataField, AutoNetworkedField]
     public int MaxLength = 50;
+
+    /// <summary>
+    /// When examining the item, should it show information about what word is recorded?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool ShowExamine = true;
+
+    /// <summary>
+    /// Should there be verbs that allow re-recording of the trigger word?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool ShowVerbs = true;
+
+    /// <summary>
+    /// The verb text that is shown when you can start recording a message.
+    /// </summary>
+    [DataField]
+    public LocId StartRecordingVerb = "trigger-on-voice-record";
+
+    /// <summary>
+    /// The verb text that is shown when you can stop recording a message.
+    /// </summary>
+    [DataField]
+    public LocId StopRecordingVerb = "trigger-on-voice-stop";
+
+    /// <summary>
+    /// Tooltip that appears when hovering over the stop or start recording verbs.
+    /// </summary>
+    [DataField]
+    public LocId? RecordingVerbMessage;
+
+    /// <summary>
+    /// The verb text that is shown when you can clear a recording.
+    /// </summary>
+    [DataField]
+    public LocId ClearRecordingVerb = "trigger-on-voice-clear";
+
+    /// <summary>
+    /// The loc string that is shown when inspecting an uninitialized voice trigger.
+    /// </summary>
+    [DataField]
+    public LocId? InspectUninitializedLoc = "trigger-on-voice-uninitialized";
+
+    /// <summary>
+    /// The loc string to use when inspecting voice trigger. Will also include the triggering phrase
+    /// </summary>
+    [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 (file)
index 0000000..8726ede
--- /dev/null
@@ -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<LockOnTriggerComponent, TriggerEvent>(OnTrigger);
+    }
+
+    private void OnTrigger(Entity<LockOnTriggerComponent> 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;
+        }
+    }
+}
index ac67cb7ed25431dd1c9c1ec8df614ab3565a8a25..c374369a7f013764cff44758f976da1130a5d7e8 100644 (file)
@@ -25,15 +25,21 @@ public sealed partial class TriggerSystem
             RemCompDeferred<ActiveListenerComponent>(ent);
     }
 
-    private void OnVoiceExamine(Entity<TriggerOnVoiceComponent> 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<TriggerOnVoiceComponent> ent, ref ListenEvent args)
     {
         var component = ent.Comp;
@@ -71,13 +77,13 @@ public sealed partial class TriggerSystem
 
     private void OnVoiceGetAltVerbs(Entity<TriggerOnVoiceComponent> ent, ref GetVerbsEvent<AlternativeVerb> 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);
index a3f9a033df214ba90cd3fd1e82d3488e91b5abe0..d1df375ccb54406e9c3ee164fd1b3a7d5c9f88ab 100644 (file)
@@ -116,7 +116,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
             }
         }
 
-        return args.CanInteract || HasComp<GhostComponent>(args.User) && !component.BlockSpectators;
+        return (args.CanInteract || HasComp<GhostComponent>(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;
+    }
 }
index 5e1e3cd7cfe53e2a31332e54e680be59476dfbdd..c645967d98b103f4e6d0b6964252f791467cf2fd 100644 (file)
@@ -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 (file)
index 0000000..fd2dc38
--- /dev/null
@@ -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}"
index 005dfedba507d591f104cfe94419060963f1e628..b80e7709d59c3f9f82fca410050282d34d33ea9c 100644 (file)
@@ -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.
index 83612ac4b2ea9874b9c73b2773c674aa7b60ef22..987c3f73400c11c4fa58e40bd175afdba9840b33 100644 (file)
@@ -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
index 936856fdf75c91d9e1886aa0a29881e7cd58dcdc..b62773fe50e5927fdb6dccf5384492f67eb3623b 100644 (file)
@@ -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
-
index 6140bcd8ede14c6e052c83ed869903c05ebfaff2..1a2ed4301ac531a9b13cab0a5b5e3b51ea3bf129 100644 (file)
@@ -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.
     - type: Fiber
       fiberMaterial: fibers-chameleon
     - type: FingerprintMask
-    - type: UserInterface
-      interfaces:
-        enum.ChameleonUiKey.Key:
-          type: ChameleonBoundUserInterface
 
 - type: entity
   parent: ClothingHandsChameleon
index 15f89da1068db4e491ded8333f75e0962355e8cf..4539e2fdf33ce69dec1ea803eb6654a88691f2ba 100644 (file)
@@ -1,5 +1,5 @@
 - type: entity
-  parent: ClothingHeadBase
+  parent: [ClothingHeadBase, BaseChameleon]
   id: ClothingHeadHatChameleon
   name: beret
   description: A beret, an artists favorite headwear.
     - type: ChameleonClothing
       slot: [HEAD]
       default: ClothingHeadHatBeret
-    - type: UserInterface
-      interfaces:
-        enum.ChameleonUiKey.Key:
-          type: ChameleonBoundUserInterface
 
 - type: entity
   parent: ClothingHeadHatFedoraBrown
index 286dfd6cf1c22aaa608b75fe0340e53e32bf9a17..33f0ec3ad60b37287ca32cccde354db30196f8bc 100644 (file)
@@ -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.
       default: ClothingMaskGas
     - type: BreathMask
     - type: IdentityBlocker # need that for default ClothingMaskGas
-    - type: UserInterface
-      interfaces:
-        enum.ChameleonUiKey.Key:
-          type: ChameleonBoundUserInterface
     - type: HideLayerClothing
       slots:
       - Snout
   suffix: Voice Mask, Chameleon
   components:
     - type: VoiceMask
+    - type: UIRequiresLock
+      userInterfaceKeys:
+        - enum.ChameleonUiKey.Key
+        - enum.VoiceMaskUIKey.Key
+      accessDeniedSound: null
+      popup: null
     - type: HideLayerClothing
       slots:
       - Snout
index b98cdd02e0c04685f539db1320664a129221f018..a32d9b5eeae76ee32d92efdb8a87b7f15055a507 100644 (file)
@@ -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
index aec34e80ca50a0939f8fab24216eaae43cfb3473..8b2116f0ca60418d4c92fe78cf59782523a22b74 100644 (file)
@@ -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.
     - 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
index ebf304557e0317e4d72333c5fe5ebb35982342fb..cfdc967a7e0ff6af8c302dfdbe35033fa95cd98b 100644 (file)
     sprite: Clothing/Shoes/Specific/wizard.rsi
 
 - type: entity
-  parent: ClothingShoesBase
+  parent: [ClothingShoesBase, BaseChameleon]
   id: ClothingShoesChameleon
   name: black shoes
   suffix: Chameleon
     - type: ChameleonClothing
       slot: [FEET]
       default: ClothingShoesColorBlack
-    - type: UserInterface
-      interfaces:
-        enum.ChameleonUiKey.Key:
-          type: ChameleonBoundUserInterface
 
 - type: entity
   parent: ClothingShoesChameleon
index a2c903bac3b77afa9285e8572dab434ea01a9f97..f7ce37fe19eee9dfc87a2c8da3766c2940f52dad 100644 (file)
@@ -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
index cb95c231e9af2cffd36bf9b5c3c6e1290f493eff..34fdbbd74b4d74345d825b9279284089e87d7219 100644 (file)
   - type: Lock
     locked: true
     unlockOnClick: false
-  - type: ActivatableUIRequiresLock
+  - type: UIRequiresLock
+    userInterfaceKeys:
+    - enum.BorgUiKey.Key
   - type: LockedWiresPanel
   - type: Damageable
     damageContainer: Silicon
index d9b2b8399392efda60c4bdc03c7955545bef3e8f..4c6facdd4fef24364878740db23711440a466f0a 100644 (file)
       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 ]
index 2082d9a152a45519b90d623ff42e049ac1d25bd4..92bd4297ad669c7bedeabe438bfc14be4ec49cc5 100644 (file)
       - MedTekCartridge
 
 - type: entity
-  parent: BasePDA
+  parent: [BasePDA, VoiceLock]
   id: ChameleonPDA
   name: passenger PDA
   description: Why isn't it gray?
index c4c5cc0aafb1765723606d001e1d8d78b9812d08..d236383226e296aa74f303514b637c3b6d14fc41 100644 (file)
 
 - type: entity
   name: passenger ID card
-  parent: IDCardStandard
+  parent: [IDCardStandard, BaseChameleon]
   id: AgentIDCard
   suffix: Agent
   components:
     - 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 (file)
index 0000000..296b9fd
--- /dev/null
@@ -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
index cbf437d0b282e937643950d59a0e2689364e6afd..2847b723d862210cb8314f5d8ec89f033a52ad14 100644 (file)
@@ -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:
index f879e2891e39037659fc48c6d6da1cbb94a8ae8e..f634425006a303c75876bbcecc5a5e4aae74f931 100644 (file)
 
 - type: entity
   name: pen
-  parent: BaseMeleeWeaponEnergy
+  parent: [BaseMeleeWeaponEnergy, VoiceLock]
   id: EnergyDagger
   suffix: E-Dagger
   description: 'A dark ink pen.'
     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
index 0662094143f8ea47da8a0936744725133b260aa5..2ab12c170504a7687bd80bb5e8570c18ed77d545 100644 (file)
@@ -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 (file)
index 0000000..6969380
--- /dev/null
@@ -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