Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0fef8d6
working yay
Apr 21, 2026
0011c19
fixed the thing I had removed
Apr 21, 2026
9b209db
there it works
Apr 21, 2026
8f1b986
i know my coding I hope
Apr 21, 2026
12e8a50
Update Resources/Textures/Interface/Actions/changeling2.rsi/meta.json
ketufaispikinut Apr 21, 2026
22d5839
Update Content.Server/VoiceMask/VoiceMaskSystem.cs
ketufaispikinut Apr 21, 2026
62d7c57
Update Content.Server/VoiceMask/VoiceMaskSystem.cs
ketufaispikinut Apr 21, 2026
a5f82b9
Update Content.Server/VoiceMask/VoiceMaskSystem.cs
ketufaispikinut Apr 21, 2026
c18458e
Update Content.Server/VoiceMask/VoiceMaskSystem.cs
ketufaispikinut Apr 21, 2026
8b9f2cb
Apply suggestions from code review
ketufaispikinut Apr 21, 2026
0c8cc18
Update changeling-catalog.ftl
ketufaispikinut Apr 21, 2026
ed9192a
Update changeling.yml
ketufaispikinut Apr 21, 2026
d32d48c
Update changeling.yml
ketufaispikinut Apr 21, 2026
0b524c2
Update StoreSystem.Ui.cs
ketufaispikinut Apr 21, 2026
8565bdb
Update StoreSystem.Ui.cs
ketufaispikinut Apr 21, 2026
1aa2369
Update StoreSystem.Ui.cs
ketufaispikinut Apr 21, 2026
f5e9ae3
Apply suggestions from code review
ketufaispikinut Apr 21, 2026
2fc234c
Update VoiceMaskSystem.cs
ketufaispikinut Apr 21, 2026
f5fc83b
fixed the bug, yay
Apr 21, 2026
5e4462a
Merge branch 'ling_voice' of https://github.com/ketufaispikinut/space…
Apr 21, 2026
f087dd6
clean
ScarKy0 Apr 21, 2026
3161545
GITHUB, DO YOUR THING
Apr 22, 2026
7531b30
Merge branch 'ling_voice' of https://github.com/ketufaispikinut/space…
Apr 22, 2026
f81bc8d
[DataField] is in the air? WRONG.
Apr 22, 2026
8b9f2ce
minor spelling mistake
Apr 22, 2026
92005a8
all is well all is well sometimes
Apr 23, 2026
1a65d17
minor fix
beck-thompson Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
return;
}

_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide);
_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide, cast.TitleText);
}

protected override void Dispose(bool disposing)
Expand Down
7 changes: 3 additions & 4 deletions Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
private List<(string, string)> _verbs = new();

private string? _verb;

public VoiceMaskNameChangeWindow()
{
RobustXamlLoader.Load(this);

NameSelectorSet.OnPressed += _ =>
{
OnNameChange?.Invoke(NameSelector.Text);
Expand Down Expand Up @@ -69,13 +68,13 @@ private void AddVerb(string name, string? verb)
SpeechVerbSelector.SelectId(id);
}

public void UpdateState(string name, string? verb, bool active, bool accentHide)
public void UpdateState(string name, string? verb, bool active, bool accentHide, LocId titleText)
{
NameSelector.Text = name;
_verb = verb;
ToggleButton.Pressed = active;
ToggleAccentButton.Pressed = accentHide;

Title = Loc.GetString(titleText);
Comment thread
ketufaispikinut marked this conversation as resolved.
for (int id = 0; id < SpeechVerbSelector.ItemCount; id++)
{
if (string.Equals(verb, SpeechVerbSelector.GetItemMetadata(id)))
Expand Down
54 changes: 47 additions & 7 deletions Content.Server/VoiceMask/VoiceMaskSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,30 @@ public sealed partial class VoiceMaskSystem : EntitySystem
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
/// <summary>
/// The name of the client-side type that represents the user interface window.
/// Used for innate voice masks, which need to be able to create their own UIs.
/// </summary>
private const string UiGeneratedName = "VoiceMaskBoundUserInterface";
Comment thread
ketufaispikinut marked this conversation as resolved.
Comment thread
ketufaispikinut marked this conversation as resolved.

// CCVar.
private int _maxNameLength;

public override void Initialize()
{
base.Initialize();
// Transform speaker name events
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameInventory);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameImplant);
SubscribeLocalEvent<VoiceMaskComponent, TransformSpeakerNameEvent>(OnInnateTransformSpeakerName);
Comment thread
ketufaispikinut marked this conversation as resolved.
// See identity attempt events
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<SeeIdentityAttemptEvent>>(OnSeeIdentityAttemptEvent);
SubscribeLocalEvent<VoiceMaskComponent, SeeIdentityAttemptEvent>(OnInnateSeeIdentityAttemptEvent);
// Transform speech events
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeechEvent>>(OnTransformSpeechInventory, before: [typeof(AccentSystem)]);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeechEvent>>(OnTransformSpeechImplant, before: [typeof(AccentSystem)]);
SubscribeLocalEvent<VoiceMaskComponent, TransformSpeechEvent>(OnTransformSpeech, before: [typeof(AccentSystem)]);
// Other events
SubscribeLocalEvent<VoiceMaskComponent, ImplantImplantedEvent>(OnImplantImplantedEvent);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRemovedEvent>(OnImplantRemovedEventEvent);
SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
Expand All @@ -49,13 +63,20 @@ public override void Initialize()
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskAccentToggleMessage>(OnAccentToggle);
SubscribeLocalEvent<VoiceMaskComponent, ClothingGotEquippedEvent>(OnEquip);
SubscribeLocalEvent<VoiceMaskSetNameEvent>(OpenUI);
SubscribeLocalEvent<VoiceMaskComponent, TransformSpeechEvent>(OnTransformSpeech, before: [typeof(AccentSystem)]);
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeechEvent>>(OnTransformSpeechInventory, before: [typeof(AccentSystem)]);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeechEvent>>(OnTransformSpeechImplant, before: [typeof(AccentSystem)]);

SubscribeLocalEvent<VoiceMaskComponent, MapInitEvent>(OnMapInit);
Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true);
}

private void OnMapInit(Entity<VoiceMaskComponent> ent, ref MapInitEvent args)
{
if (!ent.Comp.IsInnate)
return;

_actions.AddAction(ent, ent.Comp.Action);
_uiSystem.SetUi((ent, null), VoiceMaskUIKey.Key, new InterfaceData(UiGeneratedName));
_identity.QueueIdentityUpdate(ent.Owner);
}

/// <summary>
/// Hides accent if the voice mask is on and the option to block accents is on
/// </summary>
Expand All @@ -80,6 +101,11 @@ private void OnTransformSpeechImplant(Entity<VoiceMaskComponent> entity, ref Imp
TransformSpeech(entity, args.Event);
}

private void OnInnateTransformSpeakerName(Entity<VoiceMaskComponent> ent, ref TransformSpeakerNameEvent args)
{
TransformVoice(ent, args);
}
Comment thread
ketufaispikinut marked this conversation as resolved.

private void OnTransformSpeakerNameInventory(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerNameEvent> args)
{
TransformVoice(entity, args.Args);
Expand All @@ -90,6 +116,14 @@ private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, re
TransformVoice(entity, args.Event);
}

private void OnInnateSeeIdentityAttemptEvent(Entity<VoiceMaskComponent> entity, ref SeeIdentityAttemptEvent args)
{
if (!entity.Comp.OverrideIdentity || !entity.Comp.Active || !entity.Comp.IsInnate)
return;
Comment thread
ketufaispikinut marked this conversation as resolved.

args.NameOverride = GetCurrentVoiceName(entity);
}
Comment thread
ketufaispikinut marked this conversation as resolved.
Comment thread
ketufaispikinut marked this conversation as resolved.

private void OnSeeIdentityAttemptEvent(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<SeeIdentityAttemptEvent> args)
{
if (!entity.Comp.OverrideIdentity || !entity.Comp.Active)
Expand Down Expand Up @@ -139,7 +173,10 @@ private void OnChangeName(Entity<VoiceMaskComponent> entity, ref VoiceMaskChange
}

var nameUpdatedEvent = new VoiceMaskNameUpdatedEvent(entity, entity.Comp.VoiceMaskName, message.Name);
RaiseLocalEvent(message.Actor, ref nameUpdatedEvent);
if (entity.Comp.IsInnate)
RaiseLocalEvent(entity.Owner, ref nameUpdatedEvent);
else
RaiseLocalEvent(message.Actor, ref nameUpdatedEvent);

Comment thread
ketufaispikinut marked this conversation as resolved.
entity.Comp.VoiceMaskName = message.Name;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}");
Expand All @@ -156,12 +193,15 @@ private void OnToggle(Entity<VoiceMaskComponent> entity, ref VoiceMaskToggleMess

// Update identity because of possible name override
_identity.QueueIdentityUpdate(args.Actor);

UpdateUI(entity);
}

private void OnAccentToggle(Entity<VoiceMaskComponent> entity, ref VoiceMaskAccentToggleMessage args)
{
_popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-accent-toggle"), entity, args.Actor);
entity.Comp.AccentHide = !entity.Comp.AccentHide;
UpdateUI(entity);
}
#endregion

Expand All @@ -170,7 +210,7 @@ private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEqu
{
if (_lock.IsLocked(uid))
return;

_actions.AddAction(args.Wearer, ref component.ActionEntity, component.Action, uid);
}

Expand All @@ -191,7 +231,7 @@ private void OpenUI(VoiceMaskSetNameEvent ev)
private void UpdateUI(Entity<VoiceMaskComponent> entity)
{
if (_uiSystem.HasUi(entity, VoiceMaskUIKey.Key))
_uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide));
_uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide, entity.Comp.TitleText));
}
#endregion

Expand Down
4 changes: 3 additions & 1 deletion Content.Shared/VoiceMask/SharedVoiceMaskSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ public sealed class VoiceMaskBuiState : BoundUserInterfaceState
public readonly string? Verb;
public readonly bool Active;
public readonly bool AccentHide;
public readonly LocId TitleText;

public VoiceMaskBuiState(string name, string? verb, bool active, bool accentHide)
public VoiceMaskBuiState(string name, string? verb, bool active, bool accentHide, LocId titleText)
{
Name = name;
Verb = verb;
Active = active;
AccentHide = accentHide;
TitleText = titleText;
}
}

Expand Down
17 changes: 16 additions & 1 deletion Content.Shared/VoiceMask/VoiceMaskComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
namespace Content.Shared.VoiceMask;

/// <summary>
/// This component is for voice mask items! Adding this component to clothing will give the the voice mask UI
/// This component is for voice mask items & voice-masking entities! Adding this component to clothing will give the voice mask UI
/// and allow the wearer to change their voice and verb at will.
/// Having this on an entity while the IsInnate field is true will give it an innate voice masking ability.
/// </summary>
/// <remarks>
/// DO NOT use this if you do not want the interface.
Expand Down Expand Up @@ -61,5 +62,19 @@ public sealed partial class VoiceMaskComponent : Component
/// </summary>
[DataField]
public bool ChangeIDName = false;

/// <summary>
/// Whether the voice mask is innate to the entity.
/// When added to an entity while this field is set to true, the entity itself will gain the action & UI necessary to change its voice.
/// When this field is set to false, then the entity with this component will be a provider (either through implanting or through wearing) of the voice masking abilities for another entity.
/// </summary>
Comment thread
ketufaispikinut marked this conversation as resolved.
[DataField]
public bool IsInnate = false;

/// <summary>
/// Is used as the title text in the UI.
/// </summary>
[DataField]
public LocId TitleText = "voice-mask-name-change-window";
Comment thread
ketufaispikinut marked this conversation as resolved.
}

3 changes: 3 additions & 0 deletions Resources/Locale/en-US/changeling/changeling.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@ changeling-transform-bui-drop-identity-entity = Drop {$entity}
changeling-transform-bui-drop-identity-entity-popup = You dropped {$entity} from your memory.
changeling-transform-bui-drop-identity-cannot-drop = You cannot drop your current identity.

# voice mimicry
changeling-voice-mimic-window-title = Voice Mimicry

# other
changeling-paused-map-name = Changeling identity storage map
3 changes: 3 additions & 0 deletions Resources/Locale/en-US/store/changeling-catalog.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ changeling-catalog-arm-blade-desc = Transform your arm into a terrifying flesh b

changeling-catalog-flesh-clothing-name = Flesh Clothing
changeling-catalog-flesh-clothing-desc = Your body's surface will adapt to mirror the clothing of any person you are transforming into. However, these clothing items are non-functional and will make you easy to identify as a changeling if someone tries to remove them. Can be toggled.

changeling-catalog-voice-mimic-name = Voice Mimicry
changeling-catalog-voice-mimic-desc = Change your vocal coords at will to imitate existing (and imaginary) crew members. Perfect for luring in prey.
14 changes: 14 additions & 0 deletions Resources/Prototypes/Actions/changeling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,17 @@
id: ActionChangelingStore
name: DNA Store
description: Opens the ability store.

- type: entity
parent: BaseAction
id: ActionChangelingVoiceMimic
name: Voice Mimicry
description: Model your vocal cords to imitate the voice of someone else.
components:
- type: Action
itemIconStyle: BigAction
icon:
sprite: Interface/Actions/changeling2.rsi
state: mimicry
- type: InstantAction
event: !type:VoiceMaskSetNameEvent
14 changes: 14 additions & 0 deletions Resources/Prototypes/Catalog/changeling_catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@
- !type:ListingLimitedStockCondition
stock: 1
productComponents: ChangelingFleshClothingAbilityStoreDummy

- type: listing
id: ChangelingVoiceMimic
name: changeling-catalog-voice-mimic-name
description: changeling-catalog-voice-mimic-desc
icon: { sprite: /Textures/Interface/Actions/changeling2.rsi, state: mimicry}
cost:
ChangelingDNA: 10 # TODO: Balance this once we have DNA from devour. It's currently cheap for testing purposes.
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
productComponents: ChangelingVoiceMimicDummy
12 changes: 12 additions & 0 deletions Resources/Prototypes/Entities/Mobs/Player/changeling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,15 @@
belt: ChangelingFleshClothingBelt
back: ChangelingFleshClothingBack

- type: entity # dummy prototype for the store listing
id: ChangelingVoiceMimicDummy
categories: [ HideSpawnMenu ]
components:
- type: VoiceMask
action: ActionChangelingVoiceMimic
isInnate: true
changeIDName: false
# these fields are set to false as to not mess with a changeling's ability to disguise. Otherwise, newbie lings would have their status ousted as soon as they spoke with this purchase (Unknown yells, "Hello I am not a changeling")
active: false
accentHide: false
Comment thread
ketufaispikinut marked this conversation as resolved.
titleText: changeling-voice-mimic-window-title
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Made by ketufaispikinut from a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (scientist suit), a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (hydro suit) and the changeling ability border/background sprites created by TiniestShark.",
"copyright": "Made by ketufaispikinut from a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (scientist suit), a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (hydro suit) and the changeling ability border/background sprites created by TiniestShark. Mimicry by ketufaispikinut, based on TiniestShark's textures.",
"size": {
"x": 32,
"y": 32
Expand All @@ -12,6 +12,9 @@
},
{
"name": "flesh_clothing_alt"
},
{
"name": "mimicry"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading