Add voice locks to various hidden syndicate items (#39310)

This commit is contained in:
beck-thompson
2025-08-10 11:10:13 -07:00
committed by GitHub
parent 80299e863a
commit 80375370f8
36 changed files with 366 additions and 97 deletions

View File

@@ -13,6 +13,7 @@ using Content.Server.Clothing.Systems;
using Content.Server.Implants; using Content.Server.Implants;
using Content.Shared.Implants; using Content.Shared.Implants;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.PDA; using Content.Shared.PDA;
namespace Content.Server.Access.Systems namespace Content.Server.Access.Systems
@@ -25,6 +26,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChameleonClothingSystem _chameleon = default!; [Dependency] private readonly ChameleonClothingSystem _chameleon = default!;
[Dependency] private readonly ChameleonControllerSystem _chamController = default!; [Dependency] private readonly ChameleonControllerSystem _chamController = default!;
[Dependency] private readonly LockSystem _lock = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -79,7 +81,8 @@ namespace Content.Server.Access.Systems
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args) 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; return;
if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid)) if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))

View File

@@ -5,11 +5,13 @@ using Content.Shared.Chat;
using Content.Shared.Clothing; using Content.Shared.Clothing;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Speech; using Content.Shared.Speech;
using Content.Shared.VoiceMask; using Content.Shared.VoiceMask;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.VoiceMask; namespace Content.Server.VoiceMask;
@@ -22,6 +24,8 @@ public sealed partial class VoiceMaskSystem : EntitySystem
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
// CCVar. // CCVar.
private int _maxNameLength; private int _maxNameLength;
@@ -30,6 +34,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName); SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName);
SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName); SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb); SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb);
SubscribeLocalEvent<VoiceMaskComponent, ClothingGotEquippedEvent>(OnEquip); SubscribeLocalEvent<VoiceMaskComponent, ClothingGotEquippedEvent>(OnEquip);
@@ -44,6 +49,14 @@ public sealed partial class VoiceMaskSystem : EntitySystem
args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb; 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 #region User inputs from UI
private void OnChangeVerb(Entity<VoiceMaskComponent> entity, ref VoiceMaskChangeVerbMessage msg) private void OnChangeVerb(Entity<VoiceMaskComponent> entity, ref VoiceMaskChangeVerbMessage msg)
{ {
@@ -78,6 +91,9 @@ public sealed partial class VoiceMaskSystem : EntitySystem
#region UI #region UI
private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEquippedEvent args) private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEquippedEvent args)
{ {
if (_lock.IsLocked(uid))
return;
_actions.AddAction(args.Wearer, ref component.ActionEntity, component.Action, uid); _actions.AddAction(args.Wearer, ref component.ActionEntity, component.Action, uid);
} }

View File

@@ -5,6 +5,7 @@ using Content.Shared.Contraband;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
using Content.Shared.Item; using Content.Shared.Item;
using Content.Shared.Lock;
using Content.Shared.Tag; using Content.Shared.Tag;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -23,6 +24,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly TagSystem _tag = default!;
[Dependency] protected readonly IGameTiming _timing = default!; [Dependency] protected readonly IGameTiming _timing = default!;
[Dependency] private readonly LockSystem _lock = default!;
private static readonly SlotFlags[] IgnoredSlots = private static readonly SlotFlags[] IgnoredSlots =
{ {
@@ -122,7 +124,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
private void OnVerb(Entity<ChameleonClothingComponent> ent, ref GetVerbsEvent<InteractionVerb> args) 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; return;
// Can't pass args from a ref event inside of lambdas // Can't pass args from a ref event inside of lambdas

View File

@@ -78,7 +78,7 @@ public sealed class ItemToggleSystem : EntitySystem
if (ent.Comp.Activated) if (ent.Comp.Activated)
{ {
var ev = new ItemToggleActivateAttemptEvent(args.User); var ev = new ItemToggleDeactivateAttemptEvent(args.User);
RaiseLocalEvent(ent.Owner, ref ev); RaiseLocalEvent(ent.Owner, ref ev);
if (ev.Cancelled) if (ev.Cancelled)
@@ -86,7 +86,7 @@ public sealed class ItemToggleSystem : EntitySystem
} }
else else
{ {
var ev = new ItemToggleDeactivateAttemptEvent(args.User); var ev = new ItemToggleActivateAttemptEvent(args.User);
RaiseLocalEvent(ent.Owner, ref ev); RaiseLocalEvent(ent.Owner, ref ev);
if (ev.Cancelled) if (ev.Cancelled)

View File

@@ -5,13 +5,19 @@ namespace Content.Shared.Lock;
/// <summary> /// <summary>
/// This is used for toggleable items that require the entity to have a lock in a certain state. /// This is used for toggleable items that require the entity to have a lock in a certain state.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(LockSystem))]
public sealed partial class ItemToggleRequiresLockComponent : Component public sealed partial class ItemToggleRequiresLockComponent : Component
{ {
/// <summary> /// <summary>
/// TRUE: the lock must be locked to toggle the item. /// TRUE: the lock must be locked to toggle the item.
/// FALSE: the lock must be unlocked to toggle the item. /// FALSE: the lock must be unlocked to toggle the item.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public bool RequireLocked; 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";
} }

View File

@@ -21,6 +21,18 @@ public sealed partial class LockComponent : Component
[AutoNetworkedField] [AutoNetworkedField]
public bool Locked = true; 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> /// <summary>
/// Whether or not the lock is locked by simply clicking. /// Whether or not the lock is locked by simply clicking.
/// </summary> /// </summary>
@@ -44,7 +56,7 @@ public sealed partial class LockComponent : Component
/// The sound played when unlocked. /// The sound played when unlocked.
/// </summary> /// </summary>
[DataField("unlockingSound"), ViewVariables(VVAccess.ReadWrite)] [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), Params = AudioParams.Default.WithVolume(-5f),
}; };
@@ -53,7 +65,7 @@ public sealed partial class LockComponent : Component
/// The sound played when locked. /// The sound played when locked.
/// </summary> /// </summary>
[DataField("lockingSound"), ViewVariables(VVAccess.ReadWrite)] [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) Params = AudioParams.Default.WithVolume(-5f)
}; };

View File

@@ -28,12 +28,12 @@ public sealed class LockSystem : EntitySystem
{ {
[Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ActivatableUISystem _activatableUI = default!;
[Dependency] private readonly EmagSystem _emag = default!; [Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _sharedPopupSystem = default!; [Dependency] private readonly SharedPopupSystem _sharedPopupSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
/// <inheritdoc /> /// <inheritdoc />
public override void Initialize() public override void Initialize()
@@ -54,8 +54,8 @@ public sealed class LockSystem : EntitySystem
SubscribeLocalEvent<LockedWiresPanelComponent, AttemptChangePanelEvent>(OnAttemptChangePanel); SubscribeLocalEvent<LockedWiresPanelComponent, AttemptChangePanelEvent>(OnAttemptChangePanel);
SubscribeLocalEvent<LockedAnchorableComponent, UnanchorAttemptEvent>(OnUnanchorAttempt); SubscribeLocalEvent<LockedAnchorableComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<ActivatableUIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt); SubscribeLocalEvent<UIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<ActivatableUIRequiresLockComponent, LockToggledEvent>(LockToggled); SubscribeLocalEvent<UIRequiresLockComponent, LockToggledEvent>(LockToggled);
SubscribeLocalEvent<ItemToggleRequiresLockComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt); SubscribeLocalEvent<ItemToggleRequiresLockComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt);
} }
@@ -96,6 +96,9 @@ public sealed class LockSystem : EntitySystem
private void OnExamined(EntityUid uid, LockComponent lockComp, ExaminedEvent args) private void OnExamined(EntityUid uid, LockComponent lockComp, ExaminedEvent args)
{ {
if (!lockComp.ShowExamine)
return;
args.PushText(Loc.GetString(lockComp.Locked args.PushText(Loc.GetString(lockComp.Locked
? "lock-comp-on-examined-is-locked" ? "lock-comp-on-examined-is-locked"
: "lock-comp-on-examined-is-unlocked", : "lock-comp-on-examined-is-unlocked",
@@ -239,6 +242,20 @@ public sealed class LockSystem : EntitySystem
return true; 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> /// <summary>
/// Returns true if the entity is locked. /// Returns true if the entity is locked.
/// Entities with no lock component are considered unlocked. /// 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) 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; return;
AlternativeVerb verb = new() AlternativeVerb verb = new()
@@ -394,40 +411,53 @@ public sealed class LockSystem : EntitySystem
args.Cancel(); args.Cancel();
} }
private void OnUIOpenAttempt(EntityUid uid, ActivatableUIRequiresLockComponent component, ActivatableUIOpenAttemptEvent args) private void OnUIOpenAttempt(EntityUid uid, UIRequiresLockComponent component, ActivatableUIOpenAttemptEvent args)
{ {
if (args.Cancelled) if (args.Cancelled)
return; 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.Cancel(); args.Cancel();
if (lockComp.Locked) if (lockComp.Locked && component.Popup != null)
{ {
_sharedPopupSystem.PopupClient(Loc.GetString("entity-storage-component-locked-message"), uid, args.User); _sharedPopupSystem.PopupClient(Loc.GetString(component.Popup), uid, args.User);
} }
_audio.PlayPredicted(component.AccessDeniedSound, 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) if (!TryComp<LockComponent>(uid, out var lockComp) || lockComp.Locked == component.RequireLocked)
return; 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) private void OnActivateAttempt(EntityUid uid, ItemToggleRequiresLockComponent component, ref ItemToggleActivateAttemptEvent args)
{ {
if (args.Cancelled) if (args.Cancelled)
return; 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; args.Cancelled = true;
if (lockComp.Locked)
_sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-generic-fail", if (lockComp.Locked && component.LockedPopup != null)
{
_sharedPopupSystem.PopupClient(Loc.GetString(component.LockedPopup,
("target", Identity.Entity(uid, EntityManager))), ("target", Identity.Entity(uid, EntityManager))),
uid, uid,
args.User); args.User);

View 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. /// This is used for activatable UIs that require the entity to have a lock in a certain state.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))] [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> /// <summary>
/// TRUE: the lock must be locked to access the UI. /// TRUE: the lock must be locked to access the UI.
/// FALSE: the lock must be unlocked 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> /// </summary>
[DataField] [DataField]
public SoundSpecifier? AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg"); public SoundSpecifier? AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
[DataField]
public LocId? Popup = "entity-storage-component-locked-message";
} }

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -44,4 +44,52 @@ public sealed partial class TriggerOnVoiceComponent : BaseTriggerOnXComponent
/// </summary> /// </summary>
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public int MaxLength = 50; 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";
} }

View File

@@ -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;
}
}
}

View File

@@ -25,15 +25,21 @@ public sealed partial class TriggerSystem
RemCompDeferred<ActiveListenerComponent>(ent); 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) args.PushText(Loc.GetString(component.InspectUninitializedLoc));
? Loc.GetString("trigger-on-voice-uninitialized") }
: Loc.GetString("trigger-on-voice-examine", ("keyphrase", ent.Comp.KeyPhrase))); 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) private void OnListen(Entity<TriggerOnVoiceComponent> ent, ref ListenEvent args)
{ {
var component = ent.Comp; var component = ent.Comp;
@@ -71,13 +77,13 @@ public sealed partial class TriggerSystem
private void OnVoiceGetAltVerbs(Entity<TriggerOnVoiceComponent> ent, ref GetVerbsEvent<AlternativeVerb> args) private void OnVoiceGetAltVerbs(Entity<TriggerOnVoiceComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{ {
if (!args.CanInteract || !args.CanAccess) if (!args.CanInteract || !args.CanAccess || !ent.Comp.ShowVerbs)
return; return;
var user = args.User; var user = args.User;
args.Verbs.Add(new AlternativeVerb 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 = () => Act = () =>
{ {
if (ent.Comp.IsRecording) if (ent.Comp.IsRecording)
@@ -93,7 +99,7 @@ public sealed partial class TriggerSystem
args.Verbs.Add(new AlternativeVerb args.Verbs.Add(new AlternativeVerb
{ {
Text = Loc.GetString("trigger-on-voice-clear"), Text = Loc.GetString(ent.Comp.ClearRecordingVerb),
Act = () => Act = () =>
{ {
ClearRecording(ent); ClearRecording(ent);

View 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) 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. // 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). // This is so that stuff can require further conditions (like power).
var oae = new ActivatableUIOpenAttemptEvent(user); if (RaiseCanOpenEventChecks(user, uiEntity))
var uae = new UserOpenActivatableUIAttemptEvent(user, uiEntity);
RaiseLocalEvent(user, uae);
RaiseLocalEvent(uiEntity, oae);
if (oae.Cancelled || uae.Cancelled)
return false; return false;
// Give the UI an opportunity to prepare itself if it needs to do anything // 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) if (ent.Comp.InHandsOnly)
CloseAll(ent, ent); 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;
}
} }

View File

@@ -8,3 +8,5 @@ agent-id-card-current-name = Name:
agent-id-card-current-job = Job: agent-id-card-current-job = Job:
agent-id-card-job-icon-label = Job icon: agent-id-card-job-icon-label = Job icon:
agent-id-menu-title = Agent ID Card agent-id-menu-title = Agent ID Card
agent-id-open-ui-verb = Change settings

View File

@@ -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}"

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingBackpack parent: [ClothingBackpack, BaseChameleon]
id: ClothingBackpackChameleon id: ClothingBackpackChameleon
name: backpack name: backpack
description: You wear this on your back and put items into it. description: You wear this on your back and put items into it.

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingHeadsetGrey parent: [ClothingHeadsetGrey, BaseChameleon]
id: ClothingHeadsetChameleon id: ClothingHeadsetChameleon
name: passenger headset name: passenger headset
description: An updated, modular intercom that fits over the head. Takes encryption keys. description: An updated, modular intercom that fits over the head. Takes encryption keys.
@@ -14,7 +14,3 @@
- type: ChameleonClothing - type: ChameleonClothing
slot: [ears] slot: [ears]
default: ClothingHeadsetGrey default: ClothingHeadsetGrey
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingEyesBase parent: [ClothingEyesBase, BaseChameleon]
id: ClothingEyesChameleon # no flash immunity, sorry id: ClothingEyesChameleon # no flash immunity, sorry
name: sun glasses name: sun glasses
description: Useful both for security and cargonia. description: Useful both for security and cargonia.
@@ -15,8 +15,3 @@
- type: ChameleonClothing - type: ChameleonClothing
slot: [eyes] slot: [eyes]
default: ClothingEyesGlassesSunglasses default: ClothingEyesGlassesSunglasses
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingHandsButcherable parent: [ClothingHandsButcherable, BaseChameleon]
id: ClothingHandsChameleon # doesn't protect from electricity or heat id: ClothingHandsChameleon # doesn't protect from electricity or heat
name: black gloves name: black gloves
description: Regular black gloves that do not keep you from frying. description: Regular black gloves that do not keep you from frying.
@@ -17,10 +17,6 @@
- type: Fiber - type: Fiber
fiberMaterial: fibers-chameleon fiberMaterial: fibers-chameleon
- type: FingerprintMask - type: FingerprintMask
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface
- type: entity - type: entity
parent: ClothingHandsChameleon parent: ClothingHandsChameleon

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingHeadBase parent: [ClothingHeadBase, BaseChameleon]
id: ClothingHeadHatChameleon id: ClothingHeadHatChameleon
name: beret name: beret
description: A beret, an artists favorite headwear. description: A beret, an artists favorite headwear.
@@ -14,10 +14,6 @@
- type: ChameleonClothing - type: ChameleonClothing
slot: [HEAD] slot: [HEAD]
default: ClothingHeadHatBeret default: ClothingHeadHatBeret
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface
- type: entity - type: entity
parent: ClothingHeadHatFedoraBrown parent: ClothingHeadHatFedoraBrown

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingMaskBase parent: [ClothingMaskBase, BaseChameleon]
id: ClothingMaskGasChameleon id: ClothingMaskGasChameleon
name: gas mask name: gas mask
description: A face-covering mask that can be connected to an air supply. description: A face-covering mask that can be connected to an air supply.
@@ -16,10 +16,6 @@
default: ClothingMaskGas default: ClothingMaskGas
- type: BreathMask - type: BreathMask
- type: IdentityBlocker # need that for default ClothingMaskGas - type: IdentityBlocker # need that for default ClothingMaskGas
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface
- type: HideLayerClothing - type: HideLayerClothing
slots: slots:
- Snout - Snout
@@ -30,6 +26,12 @@
suffix: Voice Mask, Chameleon suffix: Voice Mask, Chameleon
components: components:
- type: VoiceMask - type: VoiceMask
- type: UIRequiresLock
userInterfaceKeys:
- enum.ChameleonUiKey.Key
- enum.VoiceMaskUIKey.Key
accessDeniedSound: null
popup: null
- type: HideLayerClothing - type: HideLayerClothing
slots: slots:
- Snout - Snout

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingNeckBase parent: [ClothingNeckBase, BaseChameleon]
id: ClothingNeckChameleon id: ClothingNeckChameleon
name: striped red scarf 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. 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 - type: ChameleonClothing
slot: [NECK] slot: [NECK]
default: ClothingNeckScarfStripedRed default: ClothingNeckScarfStripedRed
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingOuterBase parent: [ClothingOuterBase, BaseChameleon]
id: ClothingOuterChameleon id: ClothingOuterChameleon
name: vest name: vest
description: A thick vest with a rubbery, water-resistant shell. description: A thick vest with a rubbery, water-resistant shell.
@@ -14,10 +14,6 @@
- type: ChameleonClothing - type: ChameleonClothing
slot: [outerClothing] slot: [outerClothing]
default: ClothingOuterVest default: ClothingOuterVest
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface
- type: TemperatureProtection # Same as a basic winter coat. - type: TemperatureProtection # Same as a basic winter coat.
heatingCoefficient: 1.1 heatingCoefficient: 1.1
coolingCoefficient: 0.1 coolingCoefficient: 0.1

View File

@@ -163,7 +163,7 @@
sprite: Clothing/Shoes/Specific/wizard.rsi sprite: Clothing/Shoes/Specific/wizard.rsi
- type: entity - type: entity
parent: ClothingShoesBase parent: [ClothingShoesBase, BaseChameleon]
id: ClothingShoesChameleon id: ClothingShoesChameleon
name: black shoes name: black shoes
suffix: Chameleon suffix: Chameleon
@@ -197,10 +197,6 @@
- type: ChameleonClothing - type: ChameleonClothing
slot: [FEET] slot: [FEET]
default: ClothingShoesColorBlack default: ClothingShoesColorBlack
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface
- type: entity - type: entity
parent: ClothingShoesChameleon parent: ClothingShoesChameleon

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: ClothingUniformBase parent: [ClothingUniformBase, BaseChameleon]
id: ClothingUniformJumpsuitChameleon id: ClothingUniformJumpsuitChameleon
name: black jumpsuit name: black jumpsuit
description: A generic black jumpsuit with no rank markings. description: A generic black jumpsuit with no rank markings.
@@ -36,7 +36,3 @@
- type: ChameleonClothing - type: ChameleonClothing
slot: [innerclothing] slot: [innerclothing]
default: ClothingUniformJumpsuitColorBlack default: ClothingUniformJumpsuitColorBlack
- type: UserInterface
interfaces:
enum.ChameleonUiKey.Key:
type: ChameleonBoundUserInterface

View File

@@ -167,7 +167,9 @@
- type: Lock - type: Lock
locked: true locked: true
unlockOnClick: false unlockOnClick: false
- type: ActivatableUIRequiresLock - type: UIRequiresLock
userInterfaceKeys:
- enum.BorgUiKey.Key
- type: LockedWiresPanel - type: LockedWiresPanel
- type: Damageable - type: Damageable
damageContainer: Silicon damageContainer: Silicon

View File

@@ -2320,6 +2320,7 @@
types: types:
Heat : 0.2 #per second, scales with temperature & other constants 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 - type: entity
name: grenade penguin name: grenade penguin
parent: [ MobPenguin, MobCombat, BaseSyndicateContraband ] parent: [ MobPenguin, MobCombat, BaseSyndicateContraband ]

View File

@@ -1467,7 +1467,7 @@
- MedTekCartridge - MedTekCartridge
- type: entity - type: entity
parent: BasePDA parent: [BasePDA, VoiceLock]
id: ChameleonPDA id: ChameleonPDA
name: passenger PDA name: passenger PDA
description: Why isn't it gray? description: Why isn't it gray?

View File

@@ -580,7 +580,7 @@
- type: entity - type: entity
name: passenger ID card name: passenger ID card
parent: IDCardStandard parent: [IDCardStandard, BaseChameleon]
id: AgentIDCard id: AgentIDCard
suffix: Agent suffix: Agent
components: components:
@@ -595,9 +595,11 @@
- state: default - state: default
- state: idpassenger - state: idpassenger
- type: AgentIDCard - type: AgentIDCard
- type: UIRequiresLock
- type: ActivatableUI - type: ActivatableUI
key: enum.AgentIDCardUiKey.Key key: enum.AgentIDCardUiKey.Key
inHandsOnly: true inHandsOnly: true
verbText: agent-id-open-ui-verb
- type: Tag - type: Tag
tags: tags:
- DoorBumpOpener - DoorBumpOpener

View File

@@ -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

View File

@@ -53,7 +53,7 @@
- type: DisarmMalus - type: DisarmMalus
- type: entity - type: entity
parent: Cane parent: [Cane, VoiceLock]
id: CaneSheath id: CaneSheath
suffix: Empty suffix: Empty
components: components:
@@ -69,6 +69,9 @@
interfaces: interfaces:
enum.StorageUiKey.Key: enum.StorageUiKey.Key:
type: StorageBoundUserInterface type: StorageBoundUserInterface
- type: ItemSlotsLock
slots:
- item
- type: ItemSlots - type: ItemSlots
slots: slots:
item: item:

View File

@@ -159,7 +159,7 @@
- type: entity - type: entity
name: pen name: pen
parent: BaseMeleeWeaponEnergy parent: [BaseMeleeWeaponEnergy, VoiceLock]
id: EnergyDagger id: EnergyDagger
suffix: E-Dagger suffix: E-Dagger
description: 'A dark ink pen.' description: 'A dark ink pen.'
@@ -222,6 +222,16 @@
damage: damage:
types: types:
Blunt: 1 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 - type: Tag
tags: tags:
- Write - Write

View File

@@ -37,7 +37,9 @@
3: { state: can-o3, shader: "unshaded" } 3: { state: can-o3, shader: "unshaded" }
- type: ActivatableUI - type: ActivatableUI
key: enum.GasCanisterUiKey.Key key: enum.GasCanisterUiKey.Key
- type: ActivatableUIRequiresLock - type: UIRequiresLock
userInterfaceKeys:
- enum.GasCanisterUiKey.Key
- type: UserInterface - type: UserInterface
interfaces: interfaces:
enum.GasCanisterUiKey.Key: enum.GasCanisterUiKey.Key:

View File

@@ -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