]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add voice mask implant (#41551)
authorbeck-thompson <107373427+beck-thompson@users.noreply.github.com>
Sun, 7 Dec 2025 02:35:46 +0000 (18:35 -0800)
committerGitHub <noreply@github.com>
Sun, 7 Dec 2025 02:35:46 +0000 (02:35 +0000)
* Add voice mask implant

* Remove voice mask

* Voice mask implant now  overrides your identity

* voice mask implant can now be extracted, when taking out the voice mask implant it now updates your name proplery

* Simplify logic

14 files changed:
Content.Server/VoiceMask/VoiceMaskComponent.cs
Content.Server/VoiceMask/VoiceMaskSystem.cs
Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs
Content.Shared/IdentityManagement/Components/IdentityComponent.cs
Content.Shared/IdentityManagement/IdentitySystem.cs
Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs
Content.Shared/Implants/SharedSubdermalImplantSystem.cs
Content.Shared/VoiceMask/VoiceMaskSetNameEvent.cs
Resources/Locale/en-US/store/uplink-catalog.ftl
Resources/Prototypes/Actions/types.yml
Resources/Prototypes/Catalog/thief_toolbox_sets.yml
Resources/Prototypes/Catalog/uplink_catalog.yml
Resources/Prototypes/Entities/Objects/Misc/implanters.yml
Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml

index d3116f94db1475f20a103a15ad82c168cfb340a2..f7e07f2bd1af5e0e869713acf82f1a3d37b2306b 100644 (file)
@@ -26,6 +26,12 @@ public sealed partial class VoiceMaskComponent : Component
     [DataField]
     public ProtoId<SpeechVerbPrototype>? VoiceMaskSpeechVerb;
 
+    /// <summary>
+    ///     If true will override the users identity with whatever <see cref="VoiceMaskName"/> is.
+    /// </summary>
+    [DataField]
+    public bool OverrideIdentity;
+
     /// <summary>
     ///     The action that gets displayed when the voice mask is equipped.
     /// </summary>
@@ -38,3 +44,4 @@ public sealed partial class VoiceMaskComponent : Component
     [DataField]
     public EntityUid? ActionEntity;
 }
+
index 528acd58b0ad335e9a2fc368cd1357d809893d31..170bddf0828d158882fc70fd10cdf683e70430da 100644 (file)
@@ -4,6 +4,9 @@ using Content.Shared.CCVar;
 using Content.Shared.Chat;
 using Content.Shared.Clothing;
 using Content.Shared.Database;
+using Content.Shared.IdentityManagement;
+using Content.Shared.IdentityManagement.Components;
+using Content.Shared.Implants;
 using Content.Shared.Inventory;
 using Content.Shared.Lock;
 using Content.Shared.Popups;
@@ -26,6 +29,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem
     [Dependency] private readonly SharedActionsSystem _actions = default!;
     [Dependency] private readonly LockSystem _lock = default!;
     [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly IdentitySystem _identity = default!;
 
     // CCVar.
     private int _maxNameLength;
@@ -33,7 +37,11 @@ public sealed partial class VoiceMaskSystem : EntitySystem
     public override void Initialize()
     {
         base.Initialize();
-        SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName);
+        SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameInventory);
+        SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameImplant);
+        SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<SeeIdentityAttemptEvent>>(OnSeeIdentityAttemptEvent);
+        SubscribeLocalEvent<VoiceMaskComponent, ImplantImplantedEvent>(OnImplantImplantedEvent);
+        SubscribeLocalEvent<VoiceMaskComponent, ImplantRemovedEvent>(OnImplantRemovedEventEvent);
         SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
         SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName);
         SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb);
@@ -43,10 +51,30 @@ public sealed partial class VoiceMaskSystem : EntitySystem
         Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true);
     }
 
-    private void OnTransformSpeakerName(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerNameEvent> args)
+    private void OnTransformSpeakerNameInventory(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerNameEvent> args)
     {
-        args.Args.VoiceName = GetCurrentVoiceName(entity);
-        args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb;
+        TransformVoice(entity, args.Args);
+    }
+
+    private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeakerNameEvent> args)
+    {
+        TransformVoice(entity, args.Event);
+    }
+
+    private void OnSeeIdentityAttemptEvent(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<SeeIdentityAttemptEvent> args)
+    {
+        if (entity.Comp.OverrideIdentity)
+            args.Event.NameOverride = GetCurrentVoiceName(entity);
+    }
+
+    private void OnImplantImplantedEvent(Entity<VoiceMaskComponent> entity, ref ImplantImplantedEvent ev)
+    {
+        _identity.QueueIdentityUpdate(ev.Implanted);
+    }
+
+    private void OnImplantRemovedEventEvent(Entity<VoiceMaskComponent> entity, ref ImplantRemovedEvent ev)
+    {
+        _identity.QueueIdentityUpdate(ev.Implanted);
     }
 
     private void OnLockToggled(Entity<VoiceMaskComponent> ent, ref LockToggledEvent args)
@@ -79,6 +107,9 @@ public sealed partial class VoiceMaskSystem : EntitySystem
             return;
         }
 
+        var nameUpdatedEvent = new VoiceMaskNameUpdatedEvent(entity, entity.Comp.VoiceMaskName, message.Name);
+        RaiseLocalEvent(message.Actor, ref nameUpdatedEvent);
+
         entity.Comp.VoiceMaskName = message.Name;
         _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}");
 
@@ -123,5 +154,11 @@ public sealed partial class VoiceMaskSystem : EntitySystem
     {
         return entity.Comp.VoiceMaskName ?? Loc.GetString("voice-mask-default-name-override");
     }
+
+    private void TransformVoice(Entity<VoiceMaskComponent> entity, TransformSpeakerNameEvent args)
+    {
+        args.VoiceName = GetCurrentVoiceName(entity);
+        args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.SpeechVerb;
+    }
     #endregion
 }
index cc92a4c078eb5f7bdb6dc57c5eabc3a21c1cae84..5dfbc7e36cbb8dce1a0da1cd231095d481137b47 100644 (file)
@@ -37,4 +37,9 @@ public sealed class SeeIdentityAttemptEvent : CancellableEntityEventArgs, IInven
 
     // cumulative coverage from each relayed slot
     public IdentityBlockerCoverage TotalCoverage = IdentityBlockerCoverage.NONE;
+
+    /// <summary>
+    /// A specific name to override your identiy with.
+    /// </summary>
+    public string? NameOverride = null;
 }
index 4646ccc835b07bb95668c2f951fe9127c1571347..778066361f2bba9e093a5d7ad3365a3615416f6e 100644 (file)
@@ -47,10 +47,16 @@ public sealed class IdentityRepresentation
         PresumedName = presumedName;
     }
 
-    public string ToStringKnown(bool trueName)
+    /// <summary>
+    /// Get this identity as a string
+    /// </summary>
+    /// <param name="trueName">Should we show their "true" name or hide it?</param>
+    /// <param name="nameOverride">A "true name" override</param>
+    /// <returns></returns>
+    public string ToStringKnown(bool trueName, string? nameOverride)
     {
         return trueName
-            ? TrueName
+            ? nameOverride ?? TrueName
             : PresumedName ?? ToStringUnknown();
     }
 
index 6b11085715c319b6334900deb56765d603f65f20..990abd9e70824ddfd701c9544ff7b3b6fa8d9f0a 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Humanoid;
 using Content.Shared.IdentityManagement.Components;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
+using Content.Shared.VoiceMask;
 using Robust.Shared.Containers;
 using Robust.Shared.Enums;
 using Robust.Shared.GameObjects.Components.Localization;
@@ -53,6 +54,7 @@ public sealed class IdentitySystem : EntitySystem
         SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
         SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
         SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+        SubscribeLocalEvent<IdentityComponent, VoiceMaskNameUpdatedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
     }
 
     /// <summary>
@@ -197,7 +199,7 @@ public sealed class IdentitySystem : EntitySystem
         var ev = new SeeIdentityAttemptEvent();
 
         RaiseLocalEvent(target, ev);
-        return representation.ToStringKnown(!ev.Cancelled);
+        return representation.ToStringKnown(!ev.Cancelled, ev.NameOverride);
     }
 
     /// <summary>
index 4c0b2c23612f880ead98fc12ef0bf934a4b55535..774be5d9b27275cf53a37a2a66a9120e168c2142 100644 (file)
@@ -1,3 +1,5 @@
+using Content.Shared.Chat;
+using Content.Shared.IdentityManagement.Components;
 using Content.Shared.Implants.Components;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
@@ -12,6 +14,8 @@ public abstract partial class SharedSubdermalImplantSystem
         SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
         SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent);
         SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent);
+        SubscribeLocalEvent<ImplantedComponent, TransformSpeakerNameEvent>(RelayToImplantEvent);
+        SubscribeLocalEvent<ImplantedComponent, SeeIdentityAttemptEvent>(RelayToImplantEvent);
     }
 
     /// <summary>
index 630416b598c6ec5315818cc8798e17cd00000f01..a347b3db64c55d69d08726ef2df9fa2a6a784bb2 100644 (file)
@@ -169,7 +169,14 @@ public abstract partial class SharedSubdermalImplantSystem : EntitySystem
 [ByRefEvent]
 public readonly record struct ImplantImplantedEvent
 {
+    /// <summary>
+    /// The implant itself
+    /// </summary>
     public readonly EntityUid Implant;
+
+    /// <summary>
+    /// The entity getting implanted
+    /// </summary>
     public readonly EntityUid Implanted;
 
     public ImplantImplantedEvent(EntityUid implant, EntityUid implanted)
index 8e1adb194f0303b4210dca39279b24158008061f..caeb8429318cebe4aafa829583160943cff8ab24 100644 (file)
@@ -6,3 +6,11 @@ public sealed partial class VoiceMaskSetNameEvent : InstantActionEvent
 {
 }
 
+/// <summary>
+/// Raised on an entity when their voice masks name is updated
+/// </summary>
+/// <param name="VoiceMaskUid">Uid of the voice mask</param>
+/// <param name="OldName">The old name</param>
+/// <param name="NewName">The new name</param>
+[ByRefEvent]
+public readonly record struct VoiceMaskNameUpdatedEvent(EntityUid VoiceMaskUid, string? OldName, string NewName);
index f3df1dcde08b3955663cbb1b066c96815ae51548..7f0f14aed8b427b1e905d528d1e9dc3ffe07750e 100644 (file)
@@ -171,9 +171,6 @@ uplink-binary-translator-key-desc = Lets you tap into the silicons' binary chann
 uplink-hypopen-name = Hypopen
 uplink-hypopen-desc = A chemical hypospray disguised as a pen, capable of instantly injecting up to 10u of reagents. Starts empty.
 
-uplink-voice-mask-name = Voice Mask
-uplink-voice-mask-desc = A gas mask that lets you adjust your voice to whoever you can think of. Also utilizes cutting-edge chameleon technology.
-
 uplink-clothing-eyes-hud-syndicate-name = Syndicate Visor
 uplink-clothing-eyes-hud-syndicate-desc = The syndicate's professional head-up display, designed for better detection of humanoids and their subsequent elimination.
 
@@ -226,6 +223,9 @@ uplink-micro-bomb-implanter-desc = Explode on death or manual activation with th
 uplink-radio-implanter-name = Radio Implanter
 uplink-radio-implanter-desc = Implants a Syndicate radio, allowing covert communication without a headset.
 
+uplink-voice-mask-implanter-name = Voice Mask Implanter
+uplink-voice-mask-implanter-desc = Modifies your vocal cords to be able to sound like anyone you could imagine.
+
 # Bundles
 uplink-observation-kit-name = Observation Kit
 uplink-observation-kit-desc = Includes surveillance camera monitor board and security hud disguised as sunglasses.
index d5ad1f3b55e4de3b664f9e0e845b655a6fa45b6a..721508b905fc90b10ab1292a091bd93895353a85 100644 (file)
     icon: Interface/Actions/shop.png
   - type: InstantAction
     event: !type:IntrinsicStoreActionEvent
+
+- type: entity
+  parent: ActionChangeVoiceMask
+  id: ActionChangeVoiceMaskImplant
+  components:
+  - type: Action
+    icon: { sprite: Interface/Actions/voice-mask.rsi, state: icon }
+    itemIconStyle: BigAction
index 79831bedc50442b3ba0234e622110dea778f7a6d..81e3d10972ebaa9a362da1f14f6345b159e77f7b 100644 (file)
@@ -83,7 +83,7 @@
   - EncryptionKeyStationMaster
   - CyberPen
   - BriefcaseThiefBribingBundleFilled
-  - ClothingMaskGasVoiceChameleon
+  - VoiceMaskImplanter
   #- todo Chameleon Stamp
 
 - type: thiefBackpackSet
index 3a3882eae0a12409d9da651c71cc80623cef8586..3ecd49c4b48bbfaefb05a1246067d47f9ad0662f 100644 (file)
   categories:
   - UplinkImplants
 
+- type: listing
+  id: UplinkVoiceMaskImplant
+  name: uplink-voice-mask-implanter-name
+  description: uplink-voice-mask-implanter-desc
+  icon: { sprite: Interface/Actions/voice-mask.rsi, state: icon }
+  productEntity: VoiceMaskImplanter
+  discountCategory: usualDiscounts
+  discountDownTo:
+    Telecrystal: 1
+  cost:
+    Telecrystal: 2
+  categories:
+  - UplinkImplants
 
 # Wearables
 
   categories:
   - UplinkWearables
 
-- type: listing
-  id: UplinkVoiceMask
-  name: uplink-voice-mask-name
-  description: uplink-voice-mask-desc
-  productEntity: ClothingMaskGasVoiceChameleon
-  discountCategory: usualDiscounts
-  discountDownTo:
-    Telecrystal: 1
-  cost:
-    Telecrystal: 2
-  categories:
-  - UplinkWearables
-
 - type: listing
   id: UplinkHolster
   name: uplink-holster-name
index 7d2408a06dc8864375e457490fe241c7efe6a9f2..dbf281f7d1dffe7969c88f1720daf4ac59a799e6 100644 (file)
@@ -47,6 +47,7 @@
     - FakeMindShieldImplant
     - RadioImplant
     - ChameleonControllerImplant
+    - VoiceMaskImplant
     deimplantFailureDamage:
       types:
         Cellular: 50
   - type: Implanter
     implant: ChameleonControllerImplant
 
+- type: entity
+  id: VoiceMaskImplanter
+  name: voice mask implanter
+  parent: BaseImplantOnlyImplanterSyndi
+  components:
+  - type: Implanter
+    implant: VoiceMaskImplant
+
 #Nuclear Operative/Special implanters
 
 - type: entity
index 8f6acd91d6c445fd5d9c8b6b157ddcc336d0d50d..c6f545f21fc33074076248fa5544d170b6b94903 100644 (file)
       enum.ChameleonControllerKey.Key:
         type: ChameleonControllerBoundUserInterface
 
+- type: entity
+  parent: BaseSubdermalImplant
+  id: VoiceMaskImplant
+  name: voice mask implant
+  description: This implant allows you to change your voice at will.
+  categories: [ HideSpawnMenu ]
+  components:
+  - type: SubdermalImplant
+    implantAction: ActionChangeVoiceMaskImplant
+  - type: VoiceMask
+    overrideIdentity: true
+  - type: UserInterface
+    interfaces:
+      enum.VoiceMaskUIKey.Key:
+        type: VoiceMaskBoundUserInterface
+  - type: Tag
+    tags:
+    - SubdermalImplant
+
 #Nuclear Operative/Special Exclusive implants
 
 - type: entity