From 688c0b5884003b2f2cf81bcbb1842915851356b3 Mon Sep 17 00:00:00 2001
From: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Date: Tue, 26 Aug 2025 07:18:10 -0700
Subject: [PATCH] Syndicate locks are now selectable (#39532)
* Syndicate locks are now selectable
* Minor tweaks
* Make not syndicate themed
* Address refview
* review
---------
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
---
.../SelectableComponentAdderComponent.cs | 77 +++++++++++++++
.../SelectableComponentAdderSystem.cs | 95 +++++++++++++++++++
.../Triggers/TriggerOnVoiceComponent.cs | 12 +--
.../Locale/en-US/locks/selectable-locks.ftl | 3 +
.../Locale/en-US/locks/voice-trigger-lock.ftl | 3 +
.../selectable-component.ftl | 1 +
.../Entities/Objects/Devices/pda.yml | 2 +-
.../Entities/Objects/Specific/locks.yml | 31 ++++--
.../Entities/Objects/Weapons/Melee/cane.yml | 2 +-
.../Objects/Weapons/Melee/e_sword.yml | 2 +-
Resources/Prototypes/chameleon.yml | 2 +-
11 files changed, 211 insertions(+), 19 deletions(-)
create mode 100644 Content.Shared/SelectableComponentAdder/SelectableComponentAdderComponent.cs
create mode 100644 Content.Shared/SelectableComponentAdder/SelectableComponentAdderSystem.cs
create mode 100644 Resources/Locale/en-US/locks/selectable-locks.ftl
create mode 100644 Resources/Locale/en-US/selectable-component/selectable-component.ftl
diff --git a/Content.Shared/SelectableComponentAdder/SelectableComponentAdderComponent.cs b/Content.Shared/SelectableComponentAdder/SelectableComponentAdderComponent.cs
new file mode 100644
index 0000000000..5b7ccffafb
--- /dev/null
+++ b/Content.Shared/SelectableComponentAdder/SelectableComponentAdderComponent.cs
@@ -0,0 +1,77 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.SelectableComponentAdder;
+
+///
+/// Brings up a verb menu that allows players to select components that will get added to the item with this component.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SelectableComponentAdderComponent : Component
+{
+ ///
+ /// List of verb -> components to add for that verb when selected basically!
+ ///
+ [DataField(required: true)]
+ public List Entries = new();
+
+ ///
+ /// The amount of times players can make a selection and add a component. If null, there is no limit.
+ ///
+ [DataField, AutoNetworkedField]
+ public int? Selections;
+
+ ///
+ /// The verb category name that will be used.
+ ///
+ [DataField, AutoNetworkedField]
+ public LocId VerbCategoryName = "selectable-component-adder-category-name";
+}
+
+[DataDefinition]
+public sealed partial class ComponentAdderEntry
+{
+ ///
+ /// Name of the verb that will add the components in .
+ ///
+ [DataField(required: true)]
+ public LocId VerbName;
+
+ ///
+ /// Popup to show when this option is selected.
+ ///
+ [DataField(required: true)]
+ public LocId? Popup;
+
+ ///
+ /// List of all the components that will get added when the verb is selected.
+ ///
+ [DataField(required: true)]
+ public ComponentRegistry? ComponentsToAdd;
+
+ ///
+ /// The type of behavior that occurs when the component(s) already exist on the entity.
+ ///
+ [DataField]
+ public ComponentExistsSetting ComponentExistsBehavior = ComponentExistsSetting.Skip;
+
+ ///
+ /// The priorty of the verb in the list
+ ///
+ [DataField]
+ public int Priority;
+}
+
+[Serializable, NetSerializable]
+public enum ComponentExistsSetting : byte
+{
+ // If one of the components exist, skip adding it and continue adding the rest.
+ // If all components already exist, disable the verb.
+ Skip = 0,
+ // If a component already exists, replace it with the new one.
+ // The verb is always enabled.
+ Replace = 1,
+ // Disable the verb if any one of the components already exists.
+ Block = 2,
+}
diff --git a/Content.Shared/SelectableComponentAdder/SelectableComponentAdderSystem.cs b/Content.Shared/SelectableComponentAdder/SelectableComponentAdderSystem.cs
new file mode 100644
index 0000000000..29619904d7
--- /dev/null
+++ b/Content.Shared/SelectableComponentAdder/SelectableComponentAdderSystem.cs
@@ -0,0 +1,95 @@
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.SelectableComponentAdder;
+
+public sealed partial class SelectableComponentAdderSystem : EntitySystem
+{
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(OnGetVerb);
+ }
+
+ private void OnGetVerb(Entity ent, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract || ent.Comp.Selections <= 0)
+ return;
+
+ var target = args.Target;
+ var user = args.User;
+ var verbCategory = new VerbCategory(ent.Comp.VerbCategoryName, null);
+
+ foreach (var entry in ent.Comp.Entries)
+ {
+ var verb = new Verb
+ {
+ Priority = entry.Priority,
+ Category = verbCategory,
+ Disabled = CheckDisabled(target, entry.ComponentsToAdd, entry.ComponentExistsBehavior),
+ Act = () =>
+ {
+ AddComponents(target, entry.ComponentsToAdd, entry.ComponentExistsBehavior);
+ ent.Comp.Selections--;
+ Dirty(ent);
+ if (entry.Popup == null)
+ return;
+ var message = Loc.GetString(entry.Popup.Value, ("target", target));
+ _popup.PopupClient(message, target, user);
+ },
+ Text = Loc.GetString(entry.VerbName),
+ };
+ args.Verbs.Add(verb);
+ }
+ }
+
+ private bool CheckDisabled(EntityUid target, ComponentRegistry? registry, ComponentExistsSetting setting)
+ {
+ if (registry == null)
+ return false;
+
+ switch (setting)
+ {
+ case ComponentExistsSetting.Skip:
+ // disable the verb if all components already exist
+ foreach (var component in registry)
+ {
+ if (!EntityManager.HasComponent(target, Factory.GetComponent(component.Key).GetType()))
+ return false;
+ }
+ return true;
+ case ComponentExistsSetting.Replace:
+ // always allow the verb
+ return false;
+ case ComponentExistsSetting.Block:
+ // disable the verb if any component already exists.
+ foreach (var component in registry)
+ {
+ if (EntityManager.HasComponent(target, Factory.GetComponent(component.Key).GetType()))
+ return true;
+ }
+ return false;
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ private void AddComponents(EntityUid target, ComponentRegistry? registry, ComponentExistsSetting setting)
+ {
+ if (registry == null || CheckDisabled(target, registry, setting))
+ return;
+
+ foreach (var component in registry)
+ {
+ if (EntityManager.HasComponent(target, Factory.GetComponent(component.Key).GetType()) &&
+ setting is ComponentExistsSetting.Skip or ComponentExistsSetting.Block)
+ continue;
+
+ EntityManager.AddComponent(target, component.Value, true);
+ }
+ }
+}
diff --git a/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs b/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs
index 1fc3c1b966..974d5322e0 100644
--- a/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs
+++ b/Content.Shared/Trigger/Components/Triggers/TriggerOnVoiceComponent.cs
@@ -60,36 +60,36 @@ public sealed partial class TriggerOnVoiceComponent : BaseTriggerOnXComponent
///
/// The verb text that is shown when you can start recording a message.
///
- [DataField]
+ [DataField, AutoNetworkedField]
public LocId StartRecordingVerb = "trigger-on-voice-record";
///
/// The verb text that is shown when you can stop recording a message.
///
- [DataField]
+ [DataField, AutoNetworkedField]
public LocId StopRecordingVerb = "trigger-on-voice-stop";
///
/// Tooltip that appears when hovering over the stop or start recording verbs.
///
- [DataField]
+ [DataField, AutoNetworkedField]
public LocId? RecordingVerbMessage;
///
/// The verb text that is shown when you can clear a recording.
///
- [DataField]
+ [DataField, AutoNetworkedField]
public LocId ClearRecordingVerb = "trigger-on-voice-clear";
///
/// The loc string that is shown when inspecting an uninitialized voice trigger.
///
- [DataField]
+ [DataField, AutoNetworkedField]
public LocId? InspectUninitializedLoc = "trigger-on-voice-uninitialized";
///
/// The loc string to use when inspecting voice trigger. Will also include the triggering phrase
///
- [DataField]
+ [DataField, AutoNetworkedField]
public LocId? InspectInitializedLoc = "trigger-on-voice-examine";
}
diff --git a/Resources/Locale/en-US/locks/selectable-locks.ftl b/Resources/Locale/en-US/locks/selectable-locks.ftl
new file mode 100644
index 0000000000..3dbf088505
--- /dev/null
+++ b/Resources/Locale/en-US/locks/selectable-locks.ftl
@@ -0,0 +1,3 @@
+selectable-lock-verb-category-name = Add lock
+selectable-lock-verb-no-lock = No lock
+selectable-lock-verb-no-lock-popup = No lock has been added to {THE($target)}.
diff --git a/Resources/Locale/en-US/locks/voice-trigger-lock.ftl b/Resources/Locale/en-US/locks/voice-trigger-lock.ftl
index fd2dc38d23..a7069378f8 100644
--- a/Resources/Locale/en-US/locks/voice-trigger-lock.ftl
+++ b/Resources/Locale/en-US/locks/voice-trigger-lock.ftl
@@ -1,3 +1,6 @@
+voice-trigger-lock-add-verb = Voice Lock
+voice-trigger-lock-add-verb-popup = A voice lock has been added to {THE($target)}.
+
voice-trigger-lock-verb-record = Record lock phrase
voice-trigger-lock-verb-message = Locking the item will disable features that reveal its true nature!
diff --git a/Resources/Locale/en-US/selectable-component/selectable-component.ftl b/Resources/Locale/en-US/selectable-component/selectable-component.ftl
new file mode 100644
index 0000000000..f30c499f76
--- /dev/null
+++ b/Resources/Locale/en-US/selectable-component/selectable-component.ftl
@@ -0,0 +1 @@
+selectable-component-adder-category-name = Add feature
diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
index 92bd4297ad..1153047e7d 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, VoiceLock]
+ parent: [BasePDA, SelectableLock]
id: ChameleonPDA
name: passenger PDA
description: Why isn't it gray?
diff --git a/Resources/Prototypes/Entities/Objects/Specific/locks.yml b/Resources/Prototypes/Entities/Objects/Specific/locks.yml
index 296b9fd7d9..c280c4a60e 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/locks.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/locks.yml
@@ -1,5 +1,5 @@
- type: entity
- id: VoiceLock
+ id: SelectableLock
abstract: true
components:
- type: Lock
@@ -11,14 +11,27 @@
useAccess: false
unlockingSound: null # TODO: Maybe add sounds but just to the user?
lockingSound: null
+ breakOnAccessBreaker: true # more fun
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
+ - type: SelectableComponentAdder
+ selections: 1
+ entries:
+ - verbName: selectable-lock-verb-no-lock
+ popup: selectable-lock-verb-no-lock-popup
+ priority: 0
+ componentsToAdd: null
+ - verbName: voice-trigger-lock-add-verb
+ popup: voice-trigger-lock-add-verb-popup
+ priority: 1
+ componentsToAdd:
+ - 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: ActiveListener
+ - type: VoiceTriggerLock
+ verbCategoryName: selectable-lock-verb-category-name
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml
index 8b891a7533..fcf0b91f8e 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/cane.yml
@@ -54,7 +54,7 @@
- type: DisarmMalus
- type: entity
- parent: [Cane, VoiceLock]
+ parent: [Cane, SelectableLock]
id: CaneSheath
suffix: Empty
components:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml
index 5f431416e7..04b65ddb6b 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, VoiceLock]
+ parent: [BaseMeleeWeaponEnergy, SelectableLock]
id: EnergyDagger
suffix: E-Dagger
description: 'A dark ink pen.'
diff --git a/Resources/Prototypes/chameleon.yml b/Resources/Prototypes/chameleon.yml
index 6969380621..ed08c979b5 100644
--- a/Resources/Prototypes/chameleon.yml
+++ b/Resources/Prototypes/chameleon.yml
@@ -1,6 +1,6 @@
# for clothing that can be toggled, like magboots
- type: entity
- parent: VoiceLock
+ parent: SelectableLock
abstract: true
id: BaseChameleon
components:
--
2.51.2