From dd79254a0fcd4ff6e996c2effc0ef1f964355728 Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:35:46 -0800 Subject: [PATCH] Add voice mask implant (#41551) * 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 --- .../VoiceMask/VoiceMaskComponent.cs | 7 +++ Content.Server/VoiceMask/VoiceMaskSystem.cs | 45 +++++++++++++++++-- .../Components/IdentityBlockerComponent.cs | 5 +++ .../Components/IdentityComponent.cs | 10 ++++- .../IdentityManagement/IdentitySystem.cs | 4 +- .../SharedSubdermalImplantSystem.Relays.cs | 4 ++ .../Implants/SharedSubdermalImplantSystem.cs | 7 +++ .../VoiceMask/VoiceMaskSetNameEvent.cs | 8 ++++ .../Locale/en-US/store/uplink-catalog.ftl | 6 +-- Resources/Prototypes/Actions/types.yml | 8 ++++ .../Prototypes/Catalog/thief_toolbox_sets.yml | 2 +- .../Prototypes/Catalog/uplink_catalog.yml | 26 +++++------ .../Entities/Objects/Misc/implanters.yml | 9 ++++ .../Objects/Misc/subdermal_implants.yml | 19 ++++++++ 14 files changed, 136 insertions(+), 24 deletions(-) diff --git a/Content.Server/VoiceMask/VoiceMaskComponent.cs b/Content.Server/VoiceMask/VoiceMaskComponent.cs index d3116f94db..f7e07f2bd1 100644 --- a/Content.Server/VoiceMask/VoiceMaskComponent.cs +++ b/Content.Server/VoiceMask/VoiceMaskComponent.cs @@ -26,6 +26,12 @@ public sealed partial class VoiceMaskComponent : Component [DataField] public ProtoId? VoiceMaskSpeechVerb; + /// + /// If true will override the users identity with whatever is. + /// + [DataField] + public bool OverrideIdentity; + /// /// The action that gets displayed when the voice mask is equipped. /// @@ -38,3 +44,4 @@ public sealed partial class VoiceMaskComponent : Component [DataField] public EntityUid? ActionEntity; } + diff --git a/Content.Server/VoiceMask/VoiceMaskSystem.cs b/Content.Server/VoiceMask/VoiceMaskSystem.cs index 528acd58b0..170bddf082 100644 --- a/Content.Server/VoiceMask/VoiceMaskSystem.cs +++ b/Content.Server/VoiceMask/VoiceMaskSystem.cs @@ -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>(OnTransformSpeakerName); + SubscribeLocalEvent>(OnTransformSpeakerNameInventory); + SubscribeLocalEvent>(OnTransformSpeakerNameImplant); + SubscribeLocalEvent>(OnSeeIdentityAttemptEvent); + SubscribeLocalEvent(OnImplantImplantedEvent); + SubscribeLocalEvent(OnImplantRemovedEventEvent); SubscribeLocalEvent(OnLockToggled); SubscribeLocalEvent(OnChangeName); SubscribeLocalEvent(OnChangeVerb); @@ -43,10 +51,30 @@ public sealed partial class VoiceMaskSystem : EntitySystem Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true); } - private void OnTransformSpeakerName(Entity entity, ref InventoryRelayedEvent args) + private void OnTransformSpeakerNameInventory(Entity entity, ref InventoryRelayedEvent args) { - args.Args.VoiceName = GetCurrentVoiceName(entity); - args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb; + TransformVoice(entity, args.Args); + } + + private void OnTransformSpeakerNameImplant(Entity entity, ref ImplantRelayEvent args) + { + TransformVoice(entity, args.Event); + } + + private void OnSeeIdentityAttemptEvent(Entity entity, ref ImplantRelayEvent args) + { + if (entity.Comp.OverrideIdentity) + args.Event.NameOverride = GetCurrentVoiceName(entity); + } + + private void OnImplantImplantedEvent(Entity entity, ref ImplantImplantedEvent ev) + { + _identity.QueueIdentityUpdate(ev.Implanted); + } + + private void OnImplantRemovedEventEvent(Entity entity, ref ImplantRemovedEvent ev) + { + _identity.QueueIdentityUpdate(ev.Implanted); } private void OnLockToggled(Entity 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 entity, TransformSpeakerNameEvent args) + { + args.VoiceName = GetCurrentVoiceName(entity); + args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.SpeechVerb; + } #endregion } diff --git a/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs b/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs index cc92a4c078..5dfbc7e36c 100644 --- a/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs +++ b/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs @@ -37,4 +37,9 @@ public sealed class SeeIdentityAttemptEvent : CancellableEntityEventArgs, IInven // cumulative coverage from each relayed slot public IdentityBlockerCoverage TotalCoverage = IdentityBlockerCoverage.NONE; + + /// + /// A specific name to override your identiy with. + /// + public string? NameOverride = null; } diff --git a/Content.Shared/IdentityManagement/Components/IdentityComponent.cs b/Content.Shared/IdentityManagement/Components/IdentityComponent.cs index 4646ccc835..778066361f 100644 --- a/Content.Shared/IdentityManagement/Components/IdentityComponent.cs +++ b/Content.Shared/IdentityManagement/Components/IdentityComponent.cs @@ -47,10 +47,16 @@ public sealed class IdentityRepresentation PresumedName = presumedName; } - public string ToStringKnown(bool trueName) + /// + /// Get this identity as a string + /// + /// Should we show their "true" name or hide it? + /// A "true name" override + /// + public string ToStringKnown(bool trueName, string? nameOverride) { return trueName - ? TrueName + ? nameOverride ?? TrueName : PresumedName ?? ToStringUnknown(); } diff --git a/Content.Shared/IdentityManagement/IdentitySystem.cs b/Content.Shared/IdentityManagement/IdentitySystem.cs index 6b11085715..990abd9e70 100644 --- a/Content.Shared/IdentityManagement/IdentitySystem.cs +++ b/Content.Shared/IdentityManagement/IdentitySystem.cs @@ -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((uid, _, _) => QueueIdentityUpdate(uid)); SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid)); SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid)); + SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid)); } /// @@ -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); } /// diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs index 4c0b2c2361..774be5d9b2 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.Relays.cs @@ -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(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); SubscribeLocalEvent(RelayToImplantEvent); + SubscribeLocalEvent(RelayToImplantEvent); + SubscribeLocalEvent(RelayToImplantEvent); } /// diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index 630416b598..a347b3db64 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -169,7 +169,14 @@ public abstract partial class SharedSubdermalImplantSystem : EntitySystem [ByRefEvent] public readonly record struct ImplantImplantedEvent { + /// + /// The implant itself + /// public readonly EntityUid Implant; + + /// + /// The entity getting implanted + /// public readonly EntityUid Implanted; public ImplantImplantedEvent(EntityUid implant, EntityUid implanted) diff --git a/Content.Shared/VoiceMask/VoiceMaskSetNameEvent.cs b/Content.Shared/VoiceMask/VoiceMaskSetNameEvent.cs index 8e1adb194f..caeb842931 100644 --- a/Content.Shared/VoiceMask/VoiceMaskSetNameEvent.cs +++ b/Content.Shared/VoiceMask/VoiceMaskSetNameEvent.cs @@ -6,3 +6,11 @@ public sealed partial class VoiceMaskSetNameEvent : InstantActionEvent { } +/// +/// Raised on an entity when their voice masks name is updated +/// +/// Uid of the voice mask +/// The old name +/// The new name +[ByRefEvent] +public readonly record struct VoiceMaskNameUpdatedEvent(EntityUid VoiceMaskUid, string? OldName, string NewName); diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index f3df1dcde0..7f0f14aed8 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -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. diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index d5ad1f3b55..721508b905 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -488,3 +488,11 @@ 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 diff --git a/Resources/Prototypes/Catalog/thief_toolbox_sets.yml b/Resources/Prototypes/Catalog/thief_toolbox_sets.yml index 79831bedc5..81e3d10972 100644 --- a/Resources/Prototypes/Catalog/thief_toolbox_sets.yml +++ b/Resources/Prototypes/Catalog/thief_toolbox_sets.yml @@ -83,7 +83,7 @@ - EncryptionKeyStationMaster - CyberPen - BriefcaseThiefBribingBundleFilled - - ClothingMaskGasVoiceChameleon + - VoiceMaskImplanter #- todo Chameleon Stamp - type: thiefBackpackSet diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 3a3882eae0..3ecd49c4b4 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -1567,6 +1567,19 @@ 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 @@ -1583,19 +1596,6 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml index 7d2408a06d..dbf281f7d1 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml @@ -47,6 +47,7 @@ - FakeMindShieldImplant - RadioImplant - ChameleonControllerImplant + - VoiceMaskImplant deimplantFailureDamage: types: Cellular: 50 @@ -256,6 +257,14 @@ - 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 diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index 8f6acd91d6..c6f545f21f 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -239,6 +239,25 @@ 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 -- 2.52.0