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>
This commit is contained in:
Falcon 2026-06-30 20:46:27 -07:00 • committed by GitHub
parent e24d755c33
commit 9805fe3956
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 4773 additions and 160 deletions

View file

@ -1,9 +1,11 @@
using System.Linq;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Guidebook;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Speech.Components;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
@ -16,6 +18,7 @@ public sealed partial class HumanoidProfileEditor
private ColorSelectorSliders _rgbSkinColorSelector;
private List<SpeciesPrototype> _species = new();
private List<EmoteSoundsPrototype> _voices = new();
private static readonly ProtoId<GuideEntryPrototype> DefaultSpeciesGuidebook = "Species";
public void UpdateSpeciesGuidebookIcon()
@ -97,6 +100,34 @@ public sealed partial class HumanoidProfileEditor
EyeColorPicker.SetData(Profile.Appearance.EyeColor);
}
private void UpdateVoiceControls()
{
if (Profile == null)
return;
VoiceButton.Clear();
_voices.Clear();
var speciesPrototype = _prototypeManager.Index(Profile.Species);
var availableVoices = speciesPrototype.Voices;
_voices.AddRange(availableVoices.Select(protoId => _prototypeManager.Index(protoId)));
if (_voices.All(proto => Profile?.Voice != proto.ID))
SetVoice(speciesPrototype.DefaultSoundsBySex[(int)Profile.Sex]);
for (var i = 0; i < availableVoices.Count; i++)
{
var name = Loc.GetString(_voices[i].VoiceSelectorName);
VoiceButton.AddItem(name, i);
if (Profile?.Voice.Equals(_voices[i].ID) == true)
{
VoiceButton.SelectId(i);
}
}
}
private void UpdateSkinColor()
{
if (Profile == null)
@ -188,6 +219,7 @@ public sealed partial class HumanoidProfileEditor
// In case there's species restrictions for loadouts
RefreshLoadouts();
UpdateSexControls(); // update sex for new species
UpdateVoiceControls();
UpdateSpeciesGuidebookIcon();
ReloadPreview();
}
@ -215,11 +247,22 @@ public sealed partial class HumanoidProfileEditor
break;
}
// this does the same as above but for voice
if (_prototypeManager.TryIndex(Profile?.Species, out var prototype))
SetVoice(prototype.DefaultSoundsBySex[(int)newSex]);
UpdateGenderControls();
UpdateVoiceControls();
_markingsModel.SetOrganSexes(newSex);
ReloadPreview();
}
private void SetVoice(ProtoId<EmoteSoundsPrototype> newVoice)
{
Profile = Profile?.WithVoice(newVoice);
IsDirty = true;
}
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);

View file

@ -74,6 +74,12 @@
<Control HorizontalExpand="True"/>
<OptionButton Name="SexButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Voice -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-voice-label'}" />
<Control HorizontalExpand="True" />
<OptionButton Name="VoiceButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Pronouns -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-pronouns-label'}" />

View file

@ -164,6 +164,16 @@ namespace Content.Client.Lobby.UI
#endregion Sex
#region Voice
VoiceButton.OnItemSelected += args =>
{
VoiceButton.SelectId(args.Id);
SetVoice(_voices[args.Id]);
};
#endregion
#region Age
AgeEdit.OnTextChanged += args =>
@ -369,6 +379,7 @@ namespace Content.Client.Lobby.UI
UpdateNameEdit();
UpdateFlavorTextEdit();
UpdateSexControls();
UpdateVoiceControls();
UpdateGenderControls();
UpdateSkinColor();
UpdateSpawnPriorityControls();

View file

@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.IntegrationTests.Utility;
using Content.Shared.Body;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
@ -20,7 +21,9 @@ namespace Content.IntegrationTests.Tests.Humanoid;
public sealed class HumanoidProfileTests : GameTest
{
private static readonly EntProtoId BaseSpecies = "MobHuman";
private static readonly ProtoId<SpeciesPrototype> Vox = "Vox";
private static readonly ProtoId<SpeciesPrototype> SlimePerson = "SlimePerson";
public static readonly ProtoId<EmoteSoundsPrototype> SlimeVoice = "FemaleSlime";
private static string[] _species = GameDataScrounger.PrototypesOfKind<SpeciesPrototype>();
[SidedDependency(Side.Server)] private BodySystem _bodySystem = default!;
@ -45,16 +48,20 @@ public sealed class HumanoidProfileTests : GameTest
.WithSex(Sex.Female)
.WithAge(67)
.WithGender(Gender.Neuter)
.WithSpecies(Vox));
.WithSpecies(SlimePerson)
.WithVoice(SlimeVoice));
var voiceComponent = SEntMan.GetComponent<VocalComponent>(body);
Assert.That(humanoidComponent.Age, Is.EqualTo(67));
Assert.That(humanoidComponent.Sex, Is.EqualTo(Sex.Female));
Assert.That(humanoidComponent.Gender, Is.EqualTo(Gender.Neuter));
Assert.That(humanoidComponent.Species, Is.EqualTo(Vox));
Assert.That(humanoidComponent.Species, Is.EqualTo(SlimePerson));
Assert.That(humanoidComponent.Voice, Is.EqualTo(SlimeVoice));
Assert.That(voiceComponent.Sounds, Is.Not.Null, message: "the MobHuman spawned by this test needs to have sex-specific sound set");
Assert.That(voiceComponent.Sounds![Sex.Female], Is.EqualTo(voiceComponent.EmoteSounds));
var speciesProto = SProtoMan.Index(humanoidComponent.Species);
Assert.That(speciesProto.DefaultSoundsBySex[(int)Sex.Female], Is.EqualTo(voiceComponent.EmoteSounds));
});
}
@ -172,4 +179,32 @@ public sealed class HumanoidProfileTests : GameTest
}
}
}
[Test]
[TestOf(typeof(VocalComponent))]
[TestCaseSource(nameof(_species))]
[Description("Tests that if a species has corresponding UI entries for their VocalComponent, as well as sex mappings for their voice entry in speciesPrototype")]
public async Task EnsureValidVoices(string species)
{
await Server.WaitIdleAsync();
await Server.WaitAssertion(() =>
{
var proto = SProtoMan.Index<SpeciesPrototype>(species);
// Species like skeletons don't need UI entries
if (!proto.RoundStart)
return;
var voiceProtos = proto.Voices;
var sexedProtos = proto.DefaultSoundsBySex.ToHashSet();
using (Assert.EnterMultipleScope())
{
Assert.That(sexedProtos.IsSubsetOf(voiceProtos), "Species with modified `DefaultSoundsBySex` should have an entry added to the `voice` field in the `speciesPrototype`");
Assert.That(voiceProtos.Union(sexedProtos).Any(), "Species should have sex mappings for at least one voice set in speciesPrototype");
Assert.That(proto.DefaultSoundsBySex, Has.Length.EqualTo(3), "Species should declare a defaultSoundsBySex for every sex");
}
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class VoiceSelector : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "voice",
table: "profile",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "voice",
table: "profile");
}
}
}

View file

@ -21,7 +21,7 @@ namespace Content.Server.Database.Migrations.Postgres
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("ProductVersion", "10.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -1191,6 +1191,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("text")
.HasColumnName("species");
b.Property<string>("Voice")
.HasColumnType("text")
.HasColumnName("voice");
b.HasKey("Id")
.HasName("PK_profile");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class VoiceSelector : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "voice",
table: "profile",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "voice",
table: "profile");
}
}
}

View file

@ -15,7 +15,7 @@ namespace Content.Server.Database.Migrations.Sqlite
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.HasAnnotation("ProductVersion", "10.0.6");
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
@ -1126,6 +1126,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("species");
b.Property<string>("Voice")
.HasColumnType("TEXT")
.HasColumnName("voice");
b.HasKey("Id")
.HasName("PK_profile");

View file

@ -332,6 +332,7 @@ namespace Content.Server.Database
public string FlavorText { get; set; } = null!;
public int Age { get; set; }
public string Sex { get; set; } = null!;
public string? Voice { get; set; } = null!; // If null, the voice gets defaulted to the sex associated value
public string Gender { get; set; } = null!;
public string Species { get; set; } = null!;
[Column(TypeName = "jsonb")] public JsonDocument? OrganMarkings { get; set; } = null!;

View file

@ -214,6 +214,7 @@ namespace Content.Server.Database
profile.Species = humanoid.Species;
profile.Age = humanoid.Age;
profile.Sex = humanoid.Sex.ToString();
profile.Voice = humanoid.Voice.ToString();
profile.Gender = humanoid.Gender.ToString();
profile.EyeColor = appearance.EyeColor.ToHex();
profile.SkinColor = appearance.SkinColor.ToHex();

View file

@ -112,8 +112,15 @@ namespace Content.Server.Preferences.Managers
new Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>();
var species = profile.Species;
if (!_prototypeManager.HasIndex<SpeciesPrototype>(species))
if (!_prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesPrototype))
{
species = HumanoidCharacterProfile.DefaultSpecies;
speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(species);
}
var voice = profile.Voice ?? speciesPrototype.DefaultSoundsBySex[(int)sex];
if (!_prototypeManager.HasIndex(voice))
voice = speciesPrototype.DefaultSoundsBySex[(int)sex];
if (profile.OrganMarkings?.RootElement is { } element)
{
@ -175,6 +182,7 @@ namespace Content.Server.Preferences.Managers
species,
profile.Age,
sex,
voice,
gender,
new HumanoidCharacterAppearance
(

View file

@ -7,6 +7,7 @@ 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;
@ -23,7 +24,7 @@ public sealed partial class VocalSystem : EntitySystem
SubscribeLocalEvent<VocalComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<VocalComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<VocalComponent, SexChangedEvent>(OnSexChanged);
SubscribeLocalEvent<VocalComponent, VoiceChangedEvent>(OnVoiceChanged);
SubscribeLocalEvent<VocalComponent, EmoteEvent>(OnEmote);
SubscribeLocalEvent<VocalComponent, EmoteActionEvent>(OnEmoteAction);
}
@ -38,7 +39,6 @@ public sealed partial class VocalSystem : EntitySystem
return;
var targetComp = EnsureComp<VocalComponent>(target);
targetComp.Sounds = source.Comp.Sounds;
targetComp.ScreamId = source.Comp.ScreamId;
targetComp.Wilhelm = source.Comp.Wilhelm;
targetComp.WilhelmProbability = source.Comp.WilhelmProbability;
@ -51,7 +51,6 @@ public sealed partial class VocalSystem : EntitySystem
{
// try to add scream action when vocal comp added
_actions.AddAction(uid, ref component.EmoteActionEntity, component.EmoteAction);
LoadSounds(uid, component);
}
private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args)
@ -63,9 +62,9 @@ public sealed partial class VocalSystem : EntitySystem
}
}
private void OnSexChanged(EntityUid uid, VocalComponent component, SexChangedEvent args)
private void OnVoiceChanged(EntityUid uid, VocalComponent component, VoiceChangedEvent args)
{
LoadSounds(uid, component, args.NewSex);
LoadSounds(uid, component, args.NewVoice);
}
private void OnEmote(EntityUid uid, VocalComponent component, ref EmoteEvent args)
@ -110,15 +109,15 @@ public sealed partial class VocalSystem : EntitySystem
return _chat.TryPlayEmoteSound(uid, ProtoMan.Index(sounds), component.ScreamId);
}
private void LoadSounds(EntityUid uid, VocalComponent component, Sex? sex = null)
/// <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 (component.Sounds == null)
if (!TryComp<HumanoidProfileComponent>(uid, out var humanoid))
return;
sex ??= CompOrNull<HumanoidProfileComponent>(uid)?.Sex ?? Sex.Unsexed;
if (!component.Sounds.TryGetValue(sex.Value, out var protoId))
return;
protoId ??= humanoid.Voice;
if (!ProtoMan.HasIndex(protoId))
return;

View file

@ -14,6 +14,12 @@ public sealed partial class EmoteSoundsPrototype : IPrototype, IInheritingProtot
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The name used for the Voice selector in the character editor UI.
/// </summary>
[DataField]
public LocId VoiceSelectorName { get; private set; } = "humanoid-profile-editor-voice-none";
/// <inheritdoc/>
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<EmoteSoundsPrototype>))]
public string[]? Parents { get; private set; }

View file

@ -1,5 +1,7 @@
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Speech.Components;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@ -16,6 +18,14 @@ public sealed partial class HumanoidProfileComponent : Component
[DataField, AutoNetworkedField]
public Gender Gender;
/// <summary>
/// Holds the EmoteSoundsPrototype that the humanoid will use to speak with
/// To change in-game, you still have to use the <see cref="VoiceChangedEvent"/>
/// or edit the <see cref="VocalComponent"/>
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<EmoteSoundsPrototype> Voice = HumanoidCharacterProfile.DefaultVoice;
[DataField, AutoNetworkedField]
public Sex Sex;

View file

@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
@ -81,7 +82,16 @@ public sealed partial class HumanoidCharacterProfileV1
public HumanoidCharacterProfile ToV2()
{
return new(Name, FlavorText, Species, Age, Sex, Gender, Appearance.ToV2(Species), SpawnPriority, JobPriorities, PreferenceUnavailable, AntagPreferences, TraitPreferences, Loadouts);
return new(Name, FlavorText, Species, Age, Sex, GetDefaultVoice(Species, Sex), Gender, Appearance.ToV2(Species), SpawnPriority, JobPriorities, PreferenceUnavailable, AntagPreferences, TraitPreferences, Loadouts);
}
// In V2 voices are stored as a separate database entry, this picks the default for the species and sex, which would give the same voice as pre-nubody.
private ProtoId<EmoteSoundsPrototype> GetDefaultVoice(ProtoId<SpeciesPrototype> species, Sex sex)
{
var prototypeManager = IoCManager.Resolve<PrototypeManager>();
var speciesPrototye = prototypeManager.Index(species);
return speciesPrototye.DefaultSoundsBySex[(int)sex];
}
}

View file

@ -26,11 +26,12 @@ public sealed partial class HumanoidProfileSystem : EntitySystem
ent.Comp.Gender = profile.Gender;
ent.Comp.Age = profile.Age;
ent.Comp.Species = profile.Species;
ent.Comp.Voice = profile.Voice;
ent.Comp.Sex = profile.Sex;
Dirty(ent);
var sexChanged = new SexChangedEvent(ent.Comp.Sex, profile.Sex);
RaiseLocalEvent(ent, ref sexChanged);
var voiceChanged = new VoiceChangedEvent(ent.Comp.Voice, profile.Voice);
RaiseLocalEvent(ent, ref voiceChanged);
if (TryComp<GrammarComponent>(ent, out var grammar))
{

View file

@ -1,4 +1,5 @@
using Content.Shared.Body;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Dataset;
using Content.Shared.Humanoid.Markings;
using Robust.Shared.Prototypes;
@ -81,6 +82,18 @@ public sealed partial class SpeciesPrototype : IPrototype
[DataField]
public List<Sex> Sexes { get; private set; } = new() { Sex.Male, Sex.Female };
/// <summary>
/// Emote sounds prototype conversion id for every sex. This is ALWAYS in the order: Male; Female; Unsexed.
/// </summary>
[DataField]
public ProtoId<EmoteSoundsPrototype>[] DefaultSoundsBySex = ["MaleHuman", "FemaleHuman", "MaleHuman"];
/// <summary>
/// List of user selectable voices in the menu. This should at least have the same sound banks as the defaults.
/// </summary>
[DataField]
public HashSet<ProtoId<EmoteSoundsPrototype>> Voices = ["MaleHuman", "FemaleHuman"];
/// <summary>
/// Characters younger than this are too young to be hired by Nanotrasen.
/// </summary>

View file

@ -1,9 +1,4 @@
using Content.Shared.Dataset;
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared.Humanoid
namespace Content.Shared.Humanoid
{
// You need to update profile, profile editor, maybe voices and names if you want to expand this further.
public enum Sex : byte
@ -12,11 +7,4 @@ namespace Content.Shared.Humanoid
Female,
Unsexed,
}
/// <summary>
/// Raised when entity has changed their sex.
/// This doesn't handle gender changes.
/// </summary>
[ByRefEvent]
public record struct SexChangedEvent(Sex OldSex, Sex NewSex);
}

View file

@ -0,0 +1,11 @@
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Humanoid;
/// <summary>
/// Raised when entity has changed their voice.
/// This doesn't handle gender changes.
/// </summary>
[ByRefEvent]
public record struct VoiceChangedEvent(ProtoId<EmoteSoundsPrototype>? OldVoice, ProtoId<EmoteSoundsPrototype>? NewVoice);

View file

@ -2,11 +2,14 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Content.Shared.CCVar;
using Content.Shared.Chat.Prototypes;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Speech.Components;
using Content.Shared.Traits;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
@ -31,6 +34,7 @@ namespace Content.Shared.Preferences
public sealed partial class HumanoidCharacterProfile
{
public static readonly ProtoId<SpeciesPrototype> DefaultSpecies = "Human";
public static readonly ProtoId<EmoteSoundsPrototype> DefaultVoice = "MaleHuman";
private static readonly Regex RestrictedNameRegex = new(@"[^A-Za-z0-9 '\-]");
private static readonly Regex ICNameCaseRegex = new(@"^(?<word>\w)|\b(?<word>\w)(?=\w*$)");
@ -86,6 +90,9 @@ namespace Content.Shared.Preferences
[DataField]
public Sex Sex { get; private set; } = Sex.Male;
[DataField]
public ProtoId<EmoteSoundsPrototype> Voice { get; set; } = DefaultVoice;
[DataField]
public Gender Gender { get; private set; } = Gender.Male;
@ -129,6 +136,7 @@ namespace Content.Shared.Preferences
string species,
int age,
Sex sex,
ProtoId<EmoteSoundsPrototype> voice,
Gender gender,
HumanoidCharacterAppearance appearance,
SpawnPriorityPreference spawnPriority,
@ -143,6 +151,7 @@ namespace Content.Shared.Preferences
Species = species;
Age = age;
Sex = sex;
Voice = voice;
Gender = gender;
Appearance = appearance;
SpawnPriority = spawnPriority;
@ -174,6 +183,7 @@ namespace Content.Shared.Preferences
other.Species,
other.Age,
other.Sex,
other.Voice,
other.Gender,
other.Appearance.Clone(),
other.SpawnPriority,
@ -237,10 +247,12 @@ namespace Content.Shared.Preferences
var sex = Sex.Unsexed;
var age = 18;
var voice = DefaultVoice; // Banishing someone to no voice would be unfortunate
if (prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesPrototype))
{
sex = random.Pick(speciesPrototype.Sexes);
age = random.Next(speciesPrototype.MinAge, speciesPrototype.OldAge); // people don't look and keep making 119 year old characters with zero rp, cap it at middle aged
voice = speciesPrototype.DefaultSoundsBySex[(int)sex];
}
var gender = Gender.Epicene;
@ -261,6 +273,7 @@ namespace Content.Shared.Preferences
{
Name = name,
Sex = sex,
Voice = voice,
Age = age,
Gender = gender,
Species = species,
@ -288,6 +301,11 @@ namespace Content.Shared.Preferences
return new(this) { Sex = sex };
}
public HumanoidCharacterProfile WithVoice(ProtoId<EmoteSoundsPrototype> voice)
{
return new(this) { Voice = voice };
}
public HumanoidCharacterProfile WithGender(Gender gender)
{
return new(this) { Gender = gender };
@ -496,6 +514,10 @@ namespace Content.Shared.Preferences
_ => Sex.Male // Invalid enum values.
};
var voice = Voice;
if (!speciesPrototype.Voices.Contains(voice))
voice = speciesPrototype.DefaultSoundsBySex[(int)sex];
// ensure the species can be that sex and their age fits the founds
if (!speciesPrototype.Sexes.Contains(sex))
sex = speciesPrototype.Sexes[0];
@ -605,6 +627,7 @@ namespace Content.Shared.Preferences
FlavorText = flavortext;
Age = age;
Sex = sex;
Voice = voice;
Gender = gender;
Appearance = appearance;
SpawnPriority = spawnPriority;
@ -725,6 +748,7 @@ namespace Content.Shared.Preferences
hashCode.Add(Species);
hashCode.Add(Age);
hashCode.Add((int)Sex);
hashCode.Add(Voice);
hashCode.Add((int)Gender);
hashCode.Add(Appearance);
hashCode.Add((int)SpawnPriority);

View file

@ -13,14 +13,6 @@ namespace Content.Shared.Speech.Components;
[AutoGenerateComponentState]
public sealed partial class VocalComponent : Component
{
/// <summary>
/// Emote sounds prototype id for each sex (not gender).
/// Entities without <see cref="HumanoidComponent"/> considered to be <see cref="Sex.Unsexed"/>.
/// </summary>
[DataField]
[AutoNetworkedField]
public Dictionary<Sex, ProtoId<EmoteSoundsPrototype>>? Sounds;
//TODO: Wilhelm scream logic needs to be more generic
/// <summary>
/// Emote ID for screaming (for whilhelm scream)
@ -55,10 +47,11 @@ public sealed partial class VocalComponent : Component
public EntityUid? EmoteActionEntity;
/// <summary>
/// Currently loaded emote sounds prototype, based on entity sex.
/// Currently loaded emote sounds prototype, based on entity sex on humanoids.
/// Null if no valid prototype for entity sex was found.
/// Generally everything should have this set. This provides the sounds for Urists as well.
/// </summary>
[ViewVariables]
[DataField]
[AutoNetworkedField]
public ProtoId<EmoteSoundsPrototype>? EmoteSounds = null;
}

View file

@ -8,6 +8,7 @@ humanoid-profile-editor-sex-label = Sex:
humanoid-profile-editor-sex-male-text = Male
humanoid-profile-editor-sex-female-text = Female
humanoid-profile-editor-sex-unsexed-text = None
humanoid-profile-editor-voice-label = Voice:
humanoid-profile-editor-age-label = Age:
humanoid-profile-editor-skin-color-label = Skin color:
humanoid-profile-editor-species-label = Species:
@ -62,3 +63,9 @@ humanoid-profile-editor-trait-count-hint = Points available: [{$current}/{$max}]
trait-category-disabilities = Disabilities
trait-category-speech = Speech traits
trait-category-quirks = Quirks
# Voices
humanoid-profile-editor-voice-masculine = Masculine
humanoid-profile-editor-voice-feminine = Feminine
humanoid-profile-editor-voice-neutral = Neutral
humanoid-profile-editor-voice-none = Unnamed Voice

View file

@ -132,10 +132,7 @@
speechSounds: Arachnid
allowedEmotes: ['Click', 'Chitter']
- type: Vocal
sounds:
Male: UnisexArachnid
Female: UnisexArachnid
Unsexed: UnisexArachnid
emoteSounds: UnisexArachnid
- type: TypingIndicator
proto: spider
- type: CreamPied

View file

@ -153,10 +153,7 @@
speechVerb: Plant
allowedEmotes: ['Chirp']
- type: Vocal
sounds:
Male: UnisexDiona
Female: UnisexDiona
Unsexed: UnisexDiona
emoteSounds: UnisexDiona
- type: TypingIndicator
proto: diona
- type: BodyEmotes

View file

@ -59,10 +59,7 @@
layer:
- MobLayer
- type: Vocal
sounds:
Male: UnisexDwarf
Female: FemaleDwarf
Unsexed: UnisexDwarf
emoteSounds: UnisexDwarf
- type: ReplacementAccent
accent: scottish
- type: Speech

View file

@ -167,10 +167,7 @@
sprite: Mobs/Effects/burn_damage.rsi
- type: MothAccent
- type: Vocal
sounds:
Male: UnisexMoth
Female: UnisexMoth
Unsexed: UnisexMoth
emoteSounds: UnisexMoth
- type: BodyEmotes
soundsId: MothBodyEmotes
- type: MovementSpeedModifier

View file

@ -136,10 +136,7 @@
sprite: Effects/creampie.rsi
state: creampie_lizard
- type: Vocal
sounds:
Male: MaleReptilian
Female: FemaleReptilian
Unsexed: MaleReptilian
emoteSounds: MaleReptilian
- type: BodyEmotes
soundsId: ReptilianBodyEmotes
- type: Damageable

View file

@ -106,10 +106,7 @@
speechVerb: Skeleton
- type: ZombieImmune
- type: Vocal
sounds:
Male: Skeleton
Female: Skeleton
Unsexed: Skeleton
emoteSounds: Skeleton
- type: SkeletonAccent
- type: Fixtures
fixtures:

View file

@ -108,10 +108,7 @@
- type: TypingIndicator
proto: slime
- type: Vocal
sounds:
Male: MaleSlime
Female: FemaleSlime
Unsexed: MaleSlime
emoteSounds: MaleSlime
- type: Damageable
damageModifierSet: Slime
- type: PassiveDamage # Around 8 damage a minute healed

View file

@ -176,10 +176,7 @@
sprite: Effects/creampie.rsi
state: creampie_vox
- type: Vocal
sounds:
Male: UnisexVox
Female: UnisexVox
Unsexed: UnisexVox
emoteSounds: UnisexVox
- type: Butcherable
spawned:
- id: FoodMeatChicken

View file

@ -162,10 +162,7 @@
- type: TypingIndicator
proto: vulpkanin
- type: Vocal
sounds:
Male: MaleVulpkanin
Female: FemaleVulpkanin
Unsexed: MaleVulpkanin
emoteSounds: MaleVulpkanin
- type: Damageable
damageModifierSet: Vulpkanin
- type: MeleeWeapon

View file

@ -136,10 +136,7 @@
speechSounds: Alto
- type: DamageForceSay
- type: Vocal
sounds:
Male: MaleHuman
Female: FemaleHuman
Unsexed: MaleHuman
emoteSounds: MaleHuman
- type: Emoting
- type: BodyEmotes
soundsId: GeneralBodyEmotes

View file

@ -145,8 +145,7 @@
speechVerb: Robotic
speechSounds: Borg
- type: Vocal
sounds:
Unsexed: UnisexSilicon
emoteSounds: UnisexSilicon
wilhelmProbability: 0
- type: DamagedSiliconAccent
- type: UnblockableSpeech
@ -369,8 +368,7 @@
allowedEmotes:
- Laugh
- type: Vocal
sounds:
Unsexed: UnisexSiliconSyndicate
emoteSounds: UnisexSiliconSyndicate
- type: PointLight
color: "#dd200b"
- type: BypassLock
@ -430,8 +428,7 @@
allowedEmotes:
- Laugh
- type: Vocal
sounds:
Unsexed: UnisexSiliconSyndicate
emoteSounds: UnisexSiliconSyndicate
- type: PointLight
color: "#dd200b"
- type: BypassLock
@ -485,8 +482,7 @@
allowedEmotes:
- Laugh
- type: Vocal
sounds:
Unsexed: UnisexSiliconXenoborg
emoteSounds: UnisexSiliconXenoborg
wilhelmProbability: 0
- type: DamagedSiliconAccent
- type: UnblockableSpeech

View file

@ -580,10 +580,7 @@
- type: ZombieAccentOverride
accent: zombieMoth
- type: Vocal
sounds:
Male: UnisexMoth
Female: UnisexMoth
Unsexed: UnisexMoth
emoteSounds: UnisexMoth
wilhelmProbability: 0.001
- type: BodyEmotes
soundsId: MothBodyEmotes
@ -1088,10 +1085,7 @@
speechSounds: Goat
speechVerb: Goat
- type: Vocal
sounds:
Female: Goat
Male: Goat
Unsexed: Goat
emoteSounds: Goat
- type: BleatingAccent
- type: InteractionPopup
successChance: 0.2
@ -1225,10 +1219,7 @@
speechSounds: Sheep
speechVerb: Sheep
- type: Vocal
sounds:
Female: Sheep
Male: Sheep
Unsexed: Sheep
emoteSounds: Sheep
- type: BleatingAccent
- type: InteractionPopup
successChance: 0.8 #SHEEP so FRIEND!!! HUG! SQUISHY! SOFT!
@ -1515,8 +1506,7 @@
rules: ghost-role-information-nonantagonist-rules
- type: GhostTakeoverAvailable
- type: Vocal
sounds:
Unsexed: Kangaroo
emoteSounds: Kangaroo
- type: Puller
needsHands: false
- type: StaminaDamageOnHit
@ -1770,10 +1760,7 @@
speechVerb: Reptilian
allowedEmotes: ['Thump']
- type: Vocal
sounds:
Male: MaleReptilian
Female: FemaleReptilian
Unsexed: MaleReptilian
emoteSounds: MaleReptilian
- type: BodyEmotes
soundsId: ReptilianBodyEmotes
- type: TypingIndicator
@ -2056,10 +2043,7 @@
types:
Blunt: 0.1
- type: Vocal
sounds:
Male: Mouse
Female: Mouse
Unsexed: Mouse
emoteSounds: Mouse
wilhelmProbability: 0.001
# TODO: Remove CombatMode when Prototype Composition is added
- type: CombatMode
@ -2447,8 +2431,7 @@
speechSounds: Parrot
speechVerb: Parrot
- type: Vocal
sounds:
Unsexed: Parrot
emoteSounds: Parrot
- type: ActiveListener
- type: Vocalizer
- type: RadioVocalizer
@ -2737,10 +2720,7 @@
speechSounds: Arachnid
allowedEmotes: ['Click', 'Chitter']
- type: Vocal
sounds:
Male: UnisexArachnid
Female: UnisexArachnid
Unsexed: UnisexArachnid
emoteSounds: UnisexArachnid
- type: TypingIndicator
proto: spider
- type: Tag
@ -3916,10 +3896,7 @@
speechSounds: Alto
allowedEmotes: ['Chirp']
- type: Vocal
sounds:
Male: UnisexDiona
Female: UnisexDiona
Unsexed: UnisexDiona
emoteSounds: UnisexDiona
- type: Tag
tags:
- DoorBumpOpener

View file

@ -283,10 +283,7 @@
- id: FoodMeatRat
amount: 1
- type: Vocal
sounds:
Male: Mouse
Female: Mouse
Unsexed: Mouse
emoteSounds: Mouse
wilhelmProbability: 0.001
- type: Tag
tags:

View file

@ -48,10 +48,7 @@
amount: 4
# They wawa!
- type: Vocal
sounds:
Male: Scurret
Female: Scurret
Unsexed: Scurret
emoteSounds: Scurret
wilhelmProbability: 0.01 # the scug, it screams like a man
- type: Speech
speechVerb: Wawa

View file

@ -45,8 +45,7 @@
- type: TypingIndicator
proto: robot
- type: Vocal
sounds:
Unsexed: UnisexSilicon
emoteSounds: UnisexSilicon
- type: Emoting
- type: ZombieImmune
- type: ProtectedFromStepTriggers
@ -245,8 +244,7 @@
params:
variation: 0.125
- type: Vocal
sounds:
Unsexed: Cluwne
emoteSounds: Cluwne
- type: ExplodeOnTrigger
- type: Explosive
explosionType: Default

View file

@ -105,8 +105,7 @@
- type: ReplacementAccent
accent: genericAggressive
- type: Vocal
sounds:
Unsexed: Bear
emoteSounds: Bear
- type: InteractionPopup
successChance: 0.25
interactSuccessString: petting-success-bear
@ -163,8 +162,7 @@
speciesId: kangaroo
templateId: spacekangaroo
- type: Vocal
sounds:
Unsexed: Kangaroo
emoteSounds: Kangaroo
- type: ReplacementAccent
accent: kangaroo
- type: ToolRefinable
@ -266,10 +264,7 @@
speechSounds: Arachnid
allowedEmotes: ['Click', 'Chitter']
- type: Vocal
sounds:
Male: UnisexArachnid
Female: UnisexArachnid
Unsexed: UnisexArachnid
emoteSounds: UnisexArachnid
- type: TypingIndicator
proto: spider
@ -487,10 +482,7 @@
speechSounds: SheepSpace
speechVerb: Sheep
- type: Vocal
sounds:
Female: SheepSpace
Male: SheepSpace
Unsexed: SheepSpace
emoteSounds: SheepSpace
- type: entity
parent: [SimpleSpaceMobBase, BaseMobAnimal, SolutionVermin, MobNightVision]

View file

@ -90,7 +90,4 @@
- type: MindExaminable
- type: Familiar
- type: Vocal
sounds:
Male: Cerberus
Female: Cerberus
Unsexed: Cerberus
emoteSounds: Cerberus

View file

@ -136,8 +136,7 @@
allowedEmotes:
- Laugh
- type: Vocal
sounds:
Unsexed: UnisexSiliconXenoborg
emoteSounds: UnisexSiliconXenoborg
wilhelmProbability: 0
- type: IntrinsicRadioReceiver
- type: IntrinsicRadioTransmitter

View file

@ -97,8 +97,7 @@
- SecretStash
- WrappedParcel
- type: Vocal
sounds:
Unsexed: UnisexSilicon
emoteSounds: UnisexSilicon
- type: Tag
tags:
- SiliconEmotes
@ -139,8 +138,7 @@
- type: StaticPrice
price: 500
- type: Vocal
sounds:
Unsexed: UnisexSiliconSyndicate
emoteSounds: UnisexSiliconSyndicate
- type: entity
parent: PersonalAI

View file

@ -11,3 +11,9 @@
lastNames: NamesArachnidLast
sexes:
- Unsexed
defaultSoundsBySex:
- UnisexArachnid
- UnisexArachnid
- UnisexArachnid
voices:
- UnisexArachnid

View file

@ -12,3 +12,9 @@
naming: TheFirstofLast
sexes:
- Unsexed
defaultSoundsBySex:
- UnisexDiona
- UnisexDiona
- UnisexDiona
voices:
- UnisexDiona

View file

@ -5,3 +5,10 @@
prototype: MobDwarf
dollPrototype: AppearanceDwarf
skinColoration: HumanToned
defaultSoundsBySex:
- UnisexDwarf
- FemaleDwarf
- UnisexDwarf
voices:
- UnisexDwarf
- FemaleDwarf

View file

@ -9,3 +9,9 @@
maleFirstNames: NamesMothFirstMale
femaleFirstNames: NamesMothFirstFemale
lastNames: NamesMothLast
defaultSoundsBySex:
- UnisexMoth
- UnisexMoth
- UnisexMoth
voices:
- UnisexMoth

View file

@ -9,3 +9,10 @@
maleFirstNames: NamesReptilianMale
femaleFirstNames: NamesReptilianFemale
naming: FirstDashFirst
defaultSoundsBySex:
- MaleReptilian
- FemaleReptilian
- MaleReptilian
voices:
- MaleReptilian
- FemaleReptilian

View file

@ -6,3 +6,10 @@
defaultSkinTone: "#b8b8b8"
dollPrototype: AppearanceSlimePerson
skinColoration: Hues
defaultSoundsBySex:
- MaleSlime
- FemaleSlime
- MaleSlime
voices:
- MaleSlime
- FemaleSlime

View file

@ -11,3 +11,9 @@
naming: First
sexes:
- Unsexed
defaultSoundsBySex:
- UnisexVox
- UnisexVox
- UnisexVox
voices:
- UnisexVox

View file

@ -9,3 +9,10 @@
maleFirstNames: names_vulpkanin_male
femaleFirstNames: names_vulpkanin_female
lastNames: names_vulpkanin_last
defaultSoundsBySex:
- MaleVulpkanin
- FemaleVulpkanin
- MaleVulpkanin
voices:
- MaleVulpkanin
- FemaleVulpkanin

View file

@ -1,6 +1,7 @@
# species
- type: emoteSounds
id: MaleHuman
voiceSelectorName: humanoid-profile-editor-voice-masculine
params:
variation: 0.125
sounds:
@ -43,6 +44,7 @@
- type: emoteSounds
id: FemaleHuman
voiceSelectorName: humanoid-profile-editor-voice-feminine
params:
variation: 0.125
sounds:
@ -85,6 +87,7 @@
- type: emoteSounds
id: MaleReptilian
voiceSelectorName: humanoid-profile-editor-voice-masculine
params:
variation: 0.125
sounds:
@ -109,6 +112,7 @@
- type: emoteSounds
id: FemaleReptilian
voiceSelectorName: humanoid-profile-editor-voice-feminine
params:
variation: 0.125
sounds:
@ -133,6 +137,7 @@
- type: emoteSounds
id: MaleSlime
voiceSelectorName: humanoid-profile-editor-voice-masculine
sounds:
Squish:
collection: Squishes
@ -177,6 +182,7 @@
- type: emoteSounds
id: FemaleSlime
voiceSelectorName: humanoid-profile-editor-voice-feminine
sounds:
Squish:
collection: Squishes
@ -221,6 +227,7 @@
- type: emoteSounds
id: UnisexVox
voiceSelectorName: humanoid-profile-editor-voice-neutral
sounds:
Scream:
path: /Audio/Voice/Vox/shriek1.ogg
@ -254,6 +261,7 @@
- type: emoteSounds
id: UnisexDiona
voiceSelectorName: humanoid-profile-editor-voice-neutral
sounds:
Scream:
path: /Audio/Voice/Diona/diona_scream.ogg
@ -278,6 +286,7 @@
- type: emoteSounds
id: UnisexArachnid
voiceSelectorName: humanoid-profile-editor-voice-neutral
params:
variation: 0.125
sounds:
@ -302,6 +311,7 @@
- type: emoteSounds
id: UnisexDwarf
voiceSelectorName: humanoid-profile-editor-voice-masculine
sounds:
Scream:
collection: MaleScreams
@ -345,6 +355,7 @@
- type: emoteSounds
id: FemaleDwarf
voiceSelectorName: humanoid-profile-editor-voice-feminine
sounds:
Scream:
collection: FemaleScreams
@ -388,6 +399,7 @@
- type: emoteSounds
id: UnisexMoth
voiceSelectorName: humanoid-profile-editor-voice-neutral
params:
variation: 0.125
sounds:
@ -479,6 +491,7 @@
# Vulp Sounds
- type: emoteSounds
id: MaleVulpkanin
voiceSelectorName: humanoid-profile-editor-voice-masculine
params:
variation: 0.125
sounds:
@ -521,6 +534,7 @@
- type: emoteSounds
id: FemaleVulpkanin
voiceSelectorName: humanoid-profile-editor-voice-feminine
params:
variation: 0.125
sounds: