CatStation/Content.Server/Speech/EntitySystems/VocalSystem.cs
Falcon 9805fe3956
Refactor voice to separate it from sex (#40593)
* feat: separate sex from voice

* wip: tried to make it use voice collection directly

* fix: yaml

* fix: migrations

* fix: ensure valid now validates correctly

* fix: unisex mobs wouldn't have voice

* chore: fix RT version

* refactor: clean up code into subfunctions

* refactor: swap name and protoId in voice

also rename sounds to voice

* chore: add comment

* fix: fallback sounds for non-humanoid characters

* fix: tests

* fix: tests

* refactor: change requests

* refactor: convert sheepers

* refactor: move Voice to SpeciesPrototype

* chore: add tests

* fix: test IoC error

* refactor: use null coalesing in ConvertProfiles

* refactor: move vocal sex association to speciesPrototype

* style: remove space from new()

* refactor: use name from emoteSoundsPrototype for UI

* refactor: no more null Voice on profile

* refactor: make DefaultVoice a static readonly property

* fix: add default voice to HumanoidProfileComponent

* chore: remove repeated vocal component from species yaml

It's inherited from BaseSpeciesMob

* style: correct comments for refactor

* fix: add missing locale for FemaleVulpkanin

* refactor: simplify logic based off surounding code

* test: add test to make sure that sexes are properly enumerated for the voices

* fix: add back urist sounds

* style: clarify comments again

* style: last thing im changing before I sleep

* refactor: battling programming semantics

* refactor: Name on EmoteSoundsPrototype.cs to VoiceSelectorName for clarity

* revert: isDirty change

* Fix HumanoidProfileTest.EnsureValidLoading.
    The profile's voice wasnt being updated, thus the voiceChanged event was never raised and that meant that the voice component had old EmoteSounds

* oops

* refactor: do the easy change requests

* refactor: use an array for indexing sex conversion for performance

* fix master

* mistakes were made

* chore: redo migrations

---------

Co-authored-by: AJCM-git <ajcm_mail@disroot.org>
2026-06-30 23:46:27 -04:00

127 lines
4.2 KiB
C#

using Content.Server.Actions;
using Content.Server.Chat.Systems;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid;
using Content.Shared.Speech;
using Content.Shared.Speech.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.Speech.EntitySystems;
public sealed partial class VocalSystem : EntitySystem
{
[Dependency] private IRobustRandom _random = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private ChatSystem _chat = default!;
[Dependency] private ActionsSystem _actions = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VocalComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<VocalComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<VocalComponent, VoiceChangedEvent>(OnVoiceChanged);
SubscribeLocalEvent<VocalComponent, EmoteEvent>(OnEmote);
SubscribeLocalEvent<VocalComponent, EmoteActionEvent>(OnEmoteAction);
}
/// <summary>
/// Copy this component's datafields from one entity to another.
/// This can't use CopyComp because of the ScreamActionEntity DataField, which should not be copied.
/// </summary>
public void CopyComponent(Entity<VocalComponent?> source, EntityUid target)
{
if (!Resolve(source, ref source.Comp))
return;
var targetComp = EnsureComp<VocalComponent>(target);
targetComp.ScreamId = source.Comp.ScreamId;
targetComp.Wilhelm = source.Comp.Wilhelm;
targetComp.WilhelmProbability = source.Comp.WilhelmProbability;
LoadSounds(target, targetComp);
Dirty(target, targetComp);
}
private void OnMapInit(EntityUid uid, VocalComponent component, MapInitEvent args)
{
// try to add scream action when vocal comp added
_actions.AddAction(uid, ref component.EmoteActionEntity, component.EmoteAction);
}
private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args)
{
// remove scream action when component removed
if (component.EmoteActionEntity != null)
{
_actions.RemoveAction(uid, component.EmoteActionEntity);
}
}
private void OnVoiceChanged(EntityUid uid, VocalComponent component, VoiceChangedEvent args)
{
LoadSounds(uid, component, args.NewVoice);
}
private void OnEmote(EntityUid uid, VocalComponent component, ref EmoteEvent args)
{
if (args.Handled || !args.Emote.Category.HasFlag(EmoteCategory.Vocal))
return;
// snowflake case for wilhelm scream easter egg
if (args.Emote == component.ScreamId)
{
args.Handled = TryPlayScreamSound(uid, component);
return;
}
if (component.EmoteSounds is not { } sounds)
return;
// just play regular sound based on emote proto
args.Handled = _chat.TryPlayEmoteSound(uid, ProtoMan.Index(sounds), args.Emote);
}
private void OnEmoteAction(EntityUid uid, VocalComponent component, EmoteActionEvent args)
{
if (args.Handled)
return;
_chat.TryEmoteWithChat(uid, args.Emote);
args.Handled = true;
}
private bool TryPlayScreamSound(EntityUid uid, VocalComponent component)
{
if (_random.Prob(component.WilhelmProbability))
{
_audio.PlayPvs(component.Wilhelm, uid, component.Wilhelm.Params);
return true;
}
if (component.EmoteSounds is not { } sounds)
return false;
return _chat.TryPlayEmoteSound(uid, ProtoMan.Index(sounds), component.ScreamId);
}
/// <summary>
/// This only works on Humanoids. Mobs should have emoteSounds on <see cref="VocalComponent"/> set directly instead.
/// </summary>
private void LoadSounds(EntityUid uid, VocalComponent component, ProtoId<EmoteSoundsPrototype>? protoId = null)
{
if (!TryComp<HumanoidProfileComponent>(uid, out var humanoid))
return;
protoId ??= humanoid.Voice;
if (!ProtoMan.HasIndex(protoId))
return;
component.EmoteSounds = protoId;
}
}