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

View File

@ -4,6 +4,8 @@ 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;
@ -27,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;
@ -34,8 +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);
@ -45,19 +51,31 @@ 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);
}
// DeltaV - specific for implants
private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeakerNameEvent> args)
{
args.Event.VoiceName = GetCurrentVoiceName(entity);
args.Event.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Event.SpeechVerb;
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);
}
// END DeltaV
private void OnLockToggled(Entity<VoiceMaskComponent> ent, ref LockToggledEvent args)
{
@ -89,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}");
@ -133,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
}

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

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

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.Interaction;
using Content.Shared.Interaction.Events;
@ -13,7 +14,8 @@ public abstract partial class SharedSubdermalImplantSystem
SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, TransformSpeakerNameEvent>(RelayToImplantEvent); // DeltaV
SubscribeLocalEvent<ImplantedComponent, TransformSpeakerNameEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, SeeIdentityAttemptEvent>(RelayToImplantEvent);
}
/// <summary>

View File

@ -180,7 +180,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)

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

View File

@ -477,3 +477,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

View File

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

View File

@ -1579,6 +1579,21 @@
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
# Begin DeltaV Removals
# discountCategory: usualDiscounts
# discountDownTo:
# Telecrystal: 1
# End DeltaV Removals
cost:
Telecrystal: 1 # DeltaV - was 2
categories:
- UplinkImplants
# Wearables
@ -1595,21 +1610,6 @@
categories:
- 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
id: UplinkHolster
name: uplink-holster-name

View File

@ -47,6 +47,7 @@
- FakeMindShieldImplant
- RadioSyndicateImplant
- ChameleonControllerImplant
- VoiceMaskImplant
deimplantFailureDamage:
types:
Cellular: 50
@ -258,6 +259,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

View File

@ -251,6 +251,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