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
This commit is contained in:
beck-thompson 2025-12-06 18:35:46 -08:00 committed by BarryNorfolk
parent 7c7ab40ae3
commit 3720601121
15 changed files with 132 additions and 52 deletions

View File

@ -26,6 +26,12 @@ public sealed partial class VoiceMaskComponent : Component
[DataField] [DataField]
public ProtoId<SpeechVerbPrototype>? VoiceMaskSpeechVerb; public ProtoId<SpeechVerbPrototype>? VoiceMaskSpeechVerb;
/// <summary>
/// If true will override the users identity with whatever <see cref="VoiceMaskName"/> is.
/// </summary>
[DataField]
public bool OverrideIdentity;
/// <summary> /// <summary>
/// The action that gets displayed when the voice mask is equipped. /// The action that gets displayed when the voice mask is equipped.
/// </summary> /// </summary>
@ -38,3 +44,4 @@ public sealed partial class VoiceMaskComponent : Component
[DataField] [DataField]
public EntityUid? ActionEntity; public EntityUid? ActionEntity;
} }

View File

@ -4,6 +4,8 @@ using Content.Shared.CCVar;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Clothing; using Content.Shared.Clothing;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Implants; using Content.Shared.Implants;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Lock; using Content.Shared.Lock;
@ -27,6 +29,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem
[Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly LockSystem _lock = default!; [Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
// CCVar. // CCVar.
private int _maxNameLength; private int _maxNameLength;
@ -34,8 +37,11 @@ public sealed partial class VoiceMaskSystem : EntitySystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName); SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameInventory);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameImplant); SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameImplant);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<SeeIdentityAttemptEvent>>(OnSeeIdentityAttemptEvent);
SubscribeLocalEvent<VoiceMaskComponent, ImplantImplantedEvent>(OnImplantImplantedEvent);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRemovedEvent>(OnImplantRemovedEventEvent);
SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled); SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName); SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb); SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb);
@ -45,19 +51,31 @@ public sealed partial class VoiceMaskSystem : EntitySystem
Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true); 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); TransformVoice(entity, args.Args);
args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb;
} }
// DeltaV - specific for implants
private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeakerNameEvent> args) private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeakerNameEvent> args)
{ {
args.Event.VoiceName = GetCurrentVoiceName(entity); TransformVoice(entity, args.Event);
args.Event.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Event.SpeechVerb; }
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);
} }
// END DeltaV
private void OnLockToggled(Entity<VoiceMaskComponent> ent, ref LockToggledEvent args) private void OnLockToggled(Entity<VoiceMaskComponent> ent, ref LockToggledEvent args)
{ {
@ -89,6 +107,9 @@ public sealed partial class VoiceMaskSystem : EntitySystem
return; return;
} }
var nameUpdatedEvent = new VoiceMaskNameUpdatedEvent(entity, entity.Comp.VoiceMaskName, message.Name);
RaiseLocalEvent(message.Actor, ref nameUpdatedEvent);
entity.Comp.VoiceMaskName = message.Name; entity.Comp.VoiceMaskName = message.Name;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}"); _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}");
@ -133,5 +154,11 @@ public sealed partial class VoiceMaskSystem : EntitySystem
{ {
return entity.Comp.VoiceMaskName ?? Loc.GetString("voice-mask-default-name-override"); 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 #endregion
} }

View File

@ -1,20 +0,0 @@
using Content.Server.VoiceMask;
using Content.Shared.Implants;
namespace Content.Server.DV.Implants;
public sealed class SubdermalBionicSyrinxImplantSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VoiceMaskComponent, ImplantImplantedEvent>(OnInsert);
}
private void OnInsert(Entity<VoiceMaskComponent> ent, ref ImplantImplantedEvent args)
{
// Update the name so it's the entities default name. You can't take it off like a voice mask so it's important!
ent.Comp.VoiceMaskName = Name(args.Implanted);
}
}

View File

@ -37,4 +37,9 @@ public sealed class SeeIdentityAttemptEvent : CancellableEntityEventArgs, IInven
// cumulative coverage from each relayed slot // cumulative coverage from each relayed slot
public IdentityBlockerCoverage TotalCoverage = IdentityBlockerCoverage.NONE; public IdentityBlockerCoverage TotalCoverage = IdentityBlockerCoverage.NONE;
/// <summary>
/// A specific name to override your identiy with.
/// </summary>
public string? NameOverride = null;
} }

View File

@ -47,10 +47,16 @@ public sealed class IdentityRepresentation
PresumedName = presumedName; 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 return trueName
? TrueName ? nameOverride ?? TrueName
: PresumedName ?? ToStringUnknown(); : PresumedName ?? ToStringUnknown();
} }

View File

@ -8,6 +8,7 @@ using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement.Components; using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
using Content.Shared.VoiceMask;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.GameObjects.Components.Localization;
@ -53,6 +54,7 @@ public sealed class IdentitySystem : EntitySystem
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid)); SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid)); SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid)); SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, VoiceMaskNameUpdatedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
} }
/// <summary> /// <summary>
@ -197,7 +199,7 @@ public sealed class IdentitySystem : EntitySystem
var ev = new SeeIdentityAttemptEvent(); var ev = new SeeIdentityAttemptEvent();
RaiseLocalEvent(target, ev); RaiseLocalEvent(target, ev);
return representation.ToStringKnown(!ev.Cancelled); return representation.ToStringKnown(!ev.Cancelled, ev.NameOverride);
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,5 @@
using Content.Shared.Chat; // DeltaV using Content.Shared.Chat;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Implants.Components; using Content.Shared.Implants.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
@ -13,7 +14,8 @@ public abstract partial class SharedSubdermalImplantSystem
SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent); SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent); SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent); SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, TransformSpeakerNameEvent>(RelayToImplantEvent); // DeltaV SubscribeLocalEvent<ImplantedComponent, TransformSpeakerNameEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, SeeIdentityAttemptEvent>(RelayToImplantEvent);
} }
/// <summary> /// <summary>

View File

@ -180,7 +180,14 @@ public abstract partial class SharedSubdermalImplantSystem : EntitySystem
[ByRefEvent] [ByRefEvent]
public readonly record struct ImplantImplantedEvent public readonly record struct ImplantImplantedEvent
{ {
/// <summary>
/// The implant itself
/// </summary>
public readonly EntityUid Implant; public readonly EntityUid Implant;
/// <summary>
/// The entity getting implanted
/// </summary>
public readonly EntityUid Implanted; public readonly EntityUid Implanted;
public ImplantImplantedEvent(EntityUid implant, EntityUid implanted) public ImplantImplantedEvent(EntityUid implant, EntityUid implanted)

View 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);

View File

@ -173,9 +173,6 @@ uplink-binary-translator-key-desc = Lets you tap into the silicons' binary chann
uplink-hypopen-name = Hypopen 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-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-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. uplink-clothing-eyes-hud-syndicate-desc = The syndicate's professional head-up display, designed for better detection of humanoids and their subsequent elimination.
@ -228,6 +225,9 @@ uplink-micro-bomb-implanter-desc = Explode on death or manual activation with th
uplink-radio-implanter-name = Radio Implanter uplink-radio-implanter-name = Radio Implanter
uplink-radio-implanter-desc = Implants a Syndicate radio, allowing covert communication without a headset. 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 # Bundles
uplink-observation-kit-name = Observation Kit uplink-observation-kit-name = Observation Kit
uplink-observation-kit-desc = Includes surveillance camera monitor board and security hud disguised as sunglasses. uplink-observation-kit-desc = Includes surveillance camera monitor board and security hud disguised as sunglasses.

View File

@ -477,3 +477,11 @@
icon: Interface/Actions/shop.png icon: Interface/Actions/shop.png
- type: InstantAction - type: InstantAction
event: !type:IntrinsicStoreActionEvent event: !type:IntrinsicStoreActionEvent
- type: entity
parent: ActionChangeVoiceMask
id: ActionChangeVoiceMaskImplant
components:
- type: Action
icon: { sprite: Interface/Actions/voice-mask.rsi, state: icon }
itemIconStyle: BigAction

View File

@ -85,7 +85,7 @@
- CyberPen - CyberPen
- SpyCrewMonitor - SpyCrewMonitor
- BriefcaseThiefBribingBundleFilled - BriefcaseThiefBribingBundleFilled
- ClothingMaskGasVoiceChameleon - VoiceMaskImplanter
#- todo Chameleon Stamp #- todo Chameleon Stamp
- type: thiefBackpackSet - type: thiefBackpackSet

View File

@ -1579,6 +1579,21 @@
categories: categories:
- UplinkImplants - 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
# Begin DeltaV Removals
# discountCategory: usualDiscounts
# discountDownTo:
# Telecrystal: 1
# End DeltaV Removals
cost:
Telecrystal: 1 # DeltaV - was 2
categories:
- UplinkImplants
# Wearables # Wearables
@ -1595,21 +1610,6 @@
categories: categories:
- UplinkWearables - UplinkWearables
- type: listing
id: UplinkVoiceMask
name: uplink-voice-mask-name
description: uplink-voice-mask-desc
productEntity: ClothingMaskGasVoiceChameleon
# Begin DeltaV Removals
# discountCategory: usualDiscounts
# discountDownTo:
# Telecrystal: 1
# End DeltaV Removals
cost:
Telecrystal: 1 # DeltaV - was 2
categories:
- UplinkWearables
- type: listing - type: listing
id: UplinkHolster id: UplinkHolster
name: uplink-holster-name name: uplink-holster-name

View File

@ -47,6 +47,7 @@
- FakeMindShieldImplant - FakeMindShieldImplant
- RadioSyndicateImplant - RadioSyndicateImplant
- ChameleonControllerImplant - ChameleonControllerImplant
- VoiceMaskImplant
deimplantFailureDamage: deimplantFailureDamage:
types: types:
Cellular: 50 Cellular: 50
@ -258,6 +259,14 @@
- type: Implanter - type: Implanter
implant: ChameleonControllerImplant implant: ChameleonControllerImplant
- type: entity
id: VoiceMaskImplanter
name: voice mask implanter
parent: BaseImplantOnlyImplanterSyndi
components:
- type: Implanter
implant: VoiceMaskImplant
#Nuclear Operative/Special implanters #Nuclear Operative/Special implanters
- type: entity - type: entity

View File

@ -251,6 +251,25 @@
enum.ChameleonControllerKey.Key: enum.ChameleonControllerKey.Key:
type: ChameleonControllerBoundUserInterface 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 #Nuclear Operative/Special Exclusive implants
- type: entity - type: entity