diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index d963e62e491..a9e6a47ef67 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -134,18 +134,6 @@ SPDX-License-Identifier: AGPL-3.0-or-later
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index 117c480e6ef..423ac5a7bad 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -50,6 +50,9 @@
using Content.Client._Arcane.ERP.OrgansAppearance;
using Content.Client._Arcane.ERP.Preferences;
using Content.Shared._Arcane.ERP.Preferences;
+using Content.Client._Art.TTS;
+using Content.Shared._Art.TTS;
+
namespace Content.Client.Lobby.UI
{
[GenerateTypedNameReferences]
@@ -98,6 +101,7 @@ public sealed partial class HumanoidProfileEditor : BoxContainer
// One at a time.
private LoadoutWindow? _loadoutWindow;
+ private TTSTab? _ttsTab; // Art
private bool _exporting;
private bool _imaging;
@@ -242,8 +246,6 @@ public HumanoidProfileEditor(
#endregion Sex
- InitializeVoice(); // Art-TTS
-
#region Age
AgeEdit.OnTextChanged += args =>
@@ -282,15 +284,6 @@ public HumanoidProfileEditor(
#endregion
- // Arcane-start
- _cfgManager.OnValueChanged(ACCVars.UseTTS, OnUseTTSChanged, true);
-
- ToggleTTS.OnPressed += _ =>
- {
- _cfgManager.SetCVar(ACCVars.UseTTS, ToggleTTS.Pressed);
- };
- // Arcane-end
-
RefreshSpecies();
SpeciesButton.OnItemSelected += args =>
@@ -600,6 +593,8 @@ public HumanoidProfileEditor(
RefreshFlavorText();
+ RefreshVoiceTab(); // Art
+
#region Dummy
SpriteRotateLeft.OnPressed += _ =>
@@ -985,6 +980,56 @@ private void OnNsfwTagsFlavorTextChange(string content)
}
// Orion-End
+ // Art-TTS-Start
+ #region Voice
+
+ private void RefreshVoiceTab()
+ {
+ if (!_cfgManager.GetCVar(ACCVars.UseTTS))
+ return;
+
+ _ttsTab = new TTSTab();
+ var children = new List();
+ foreach (var child in TabContainer.Children)
+ children.Add(child);
+
+ TabContainer.RemoveAllChildren();
+
+ for (int i = 0; i < children.Count; i++)
+ {
+ if (i == 1) // Set the tab to the 2nd place.
+ {
+ TabContainer.AddChild(_ttsTab);
+ }
+ TabContainer.AddChild(children[i]);
+ }
+
+ TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-voice-tab"));
+
+ _ttsTab.OnVoiceSelected += voiceId =>
+ {
+ SetVoice(voiceId);
+ _ttsTab.SetSelectedVoice(voiceId);
+ };
+
+ _ttsTab.OnPreviewRequested += voiceId =>
+ {
+ _entManager.System().RequestGlobalTTS(VoiceRequestType.Preview, voiceId);
+ };
+ }
+
+ private void UpdateTTSVoicesControls()
+ {
+ if (Profile is null || _ttsTab is null)
+ return;
+
+ _ttsTab.UpdateControls(Profile, Profile.Sex);
+ _ttsTab.SetSelectedVoice(Profile.Voice);
+ }
+
+ #endregion
+ // Art-TTS-End
+
///
/// Refreshes traits selector
///
@@ -2486,12 +2531,5 @@ private static void SetFlavorPreviewMarkup(RichTextLabel label, string content)
label.SetMessage(FormattedMessage.FromMarkupPermissive(safeContent), SafeMarkupTags.Basic);
}
// Orion-End
-
- // Arcane-start
- private void OnUseTTSChanged(bool value)
- {
- ToggleTTS.Pressed = value;
- }
- // Arcane-end
}
}
diff --git a/Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs b/Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs
deleted file mode 100644
index 360b40389c1..00000000000
--- a/Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using System.Linq;
-using Content.Client._Art.TTS;
-using Content.Shared._Art.TTS;
-using Content.Shared.Preferences;
-
-// ReSharper disable InconsistentNaming
-// ReSharper disable once CheckNamespace
-namespace Content.Client.Lobby.UI;
-
-public sealed partial class HumanoidProfileEditor
-{
- private List _voiceList = new();
-
- private void InitializeVoice()
- {
- _voiceList = _prototypeManager
- .EnumeratePrototypes()
- .Where(o => o.RoundStart)
- .OrderBy(o => Loc.GetString(o.Name))
- .ToList();
-
- VoiceButton.OnItemSelected += args =>
- {
- VoiceButton.SelectId(args.Id);
- SetVoice(_voiceList[args.Id].ID);
- };
-
- VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
- }
-
- private void UpdateTTSVoicesControls()
- {
- if (Profile is null)
- return;
-
- VoiceButton.Clear();
-
- var firstVoiceChoiceId = 1;
- for (var i = 0; i < _voiceList.Count; i++)
- {
- var voice = _voiceList[i];
- if (!HumanoidCharacterProfile.CanHaveVoice(voice, Profile.Sex))
- continue;
-
- var name = Loc.GetString(voice.Name);
- VoiceButton.AddItem(name, i);
-
- if (firstVoiceChoiceId == 1)
- firstVoiceChoiceId = i;
- }
-
- var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice);
- if (!VoiceButton.TrySelectId(voiceChoiceId) &&
- VoiceButton.TrySelectId(firstVoiceChoiceId))
- {
- SetVoice(_voiceList[firstVoiceChoiceId].ID);
- }
- }
-
- private void PlayPreviewTTS()
- {
- if (Profile is null)
- return;
-
- _entManager.System().RequestGlobalTTS(VoiceRequestType.Preview, Profile.Voice);
- }
-}
diff --git a/Content.Client/_Art/TTS/TTSSystem.cs b/Content.Client/_Art/TTS/TTSSystem.cs
index 210ee3cf1e9..e57cd22e599 100644
--- a/Content.Client/_Art/TTS/TTSSystem.cs
+++ b/Content.Client/_Art/TTS/TTSSystem.cs
@@ -116,6 +116,8 @@ private void OnPlayTTS(PlayTTSEvent ev)
if (ev.SourceUid != null)
{
+ if (!TryGetEntity(ev.SourceUid.Value, out _))
+ return;
var sourceUid = GetEntity(ev.SourceUid.Value);
if (sourceUid.IsValid())
_audio.PlayEntity(audioResource.AudioStream, sourceUid, null, audioParams);
diff --git a/Content.Client/_Art/TTS/TTSTab.xaml b/Content.Client/_Art/TTS/TTSTab.xaml
new file mode 100644
index 00000000000..633cebc4de0
--- /dev/null
+++ b/Content.Client/_Art/TTS/TTSTab.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Art/TTS/TTSTab.xaml.cs b/Content.Client/_Art/TTS/TTSTab.xaml.cs
new file mode 100644
index 00000000000..f66075f9df0
--- /dev/null
+++ b/Content.Client/_Art/TTS/TTSTab.xaml.cs
@@ -0,0 +1,169 @@
+using System.Linq;
+using Content.Shared._Art.TTS;
+using Content.Shared.Preferences;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+using Content.Client.Stylesheets;
+using Content.Shared.Humanoid;
+
+namespace Content.Client._Art.TTS;
+
+[GenerateTypedNameReferences]
+public sealed partial class TTSTab : Control
+{
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
+
+ public event Action? OnVoiceSelected;
+ public event Action? OnPreviewRequested;
+
+ private List _allVoices = new();
+ private List _filteredVoices = new();
+ private string? _selectedVoiceId;
+ private Sex _selectedSex = Sex.Male;
+
+ public TTSTab()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ var sexGroup = new ButtonGroup(isNoneSetAllowed: false);
+ MaleButton.Group = sexGroup;
+ FemaleButton.Group = sexGroup;
+
+ MaleButton.OnPressed += _ => SetSelectedSex(Sex.Male);
+ FemaleButton.OnPressed += _ => SetSelectedSex(Sex.Female);
+ SearchEdit.OnTextChanged += OnSearchChanged;
+ }
+
+ private void SetSelectedSex(Sex sex)
+ {
+ _selectedSex = sex;
+ if (sex == Sex.Male)
+ MaleButton.Pressed = true;
+ else
+ FemaleButton.Pressed = true;
+ UpdateResults();
+ }
+
+ private void OnSearchChanged(LineEdit.LineEditEventArgs args)
+ {
+ UpdateResults();
+ }
+
+ private void UpdateResults()
+ {
+ VoicesGrid.RemoveAllChildren();
+ _filteredVoices.Clear();
+
+ var searchText = SearchEdit.Text.ToLowerInvariant();
+ var sexCount = 0;
+
+ foreach (var voice in _allVoices)
+ {
+ if (!HumanoidCharacterProfile.CanHaveVoice(voice, _selectedSex))
+ continue;
+
+ sexCount++;
+
+ var name = Loc.GetString(voice.Name).ToLowerInvariant();
+
+ if (string.IsNullOrEmpty(searchText) ||
+ name.Contains(searchText) ||
+ voice.ID.ToLowerInvariant().Contains(searchText))
+ {
+ _filteredVoices.Add(voice);
+ }
+ }
+
+ foreach (var voice in _filteredVoices)
+ {
+ var displayName = Loc.GetString(voice.Name);
+
+ var voiceContainer = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ VerticalAlignment = VAlignment.Center
+ };
+
+ var selectButton = new Button
+ {
+ Text = displayName,
+ ToolTip = voice.ID,
+ HorizontalExpand = true,
+ StyleClasses = { StyleNano.ButtonOpenRight }
+ };
+
+ if (voice.ID == _selectedVoiceId)
+ {
+ selectButton.AddStyleClass(StyleBase.ButtonCaution);
+ }
+
+ selectButton.OnPressed += _ =>
+ {
+ OnVoiceSelected?.Invoke(voice.ID);
+ };
+
+ var previewButton = new Button
+ {
+ Text = Loc.GetString("humanoid-profile-editor-voice-play"),
+ MinWidth = 30,
+ ToolTip = Loc.GetString("humanoid-profile-editor-voice-tooltip-play"),
+ StyleClasses = { StyleNano.ButtonOpenLeft }
+ };
+
+ previewButton.OnPressed += _ =>
+ {
+ OnPreviewRequested?.Invoke(voice.ID);
+ };
+
+ voiceContainer.AddChild(selectButton);
+ voiceContainer.AddChild(previewButton);
+
+ VoicesGrid.AddChild(voiceContainer);
+ }
+
+ ResultsLabel.Text = Loc.GetString("humanoid-profile-editor-voice-match",
+ ("filtered", _filteredVoices.Count), ("all", sexCount));
+ }
+
+ public void UpdateControls(HumanoidCharacterProfile? profile, Sex sex)
+ {
+ if (profile == null)
+ return;
+
+ _selectedVoiceId = profile.Voice;
+
+ _allVoices = _prototypeManager
+ .EnumeratePrototypes()
+ .Where(o => o.RoundStart)
+ .OrderBy(o => Loc.GetString(o.Name))
+ .ToList();
+
+ SetSelectedSex(MapSex(sex));
+ }
+
+ ///
+ /// Voices are not restricted to a specific sex, but the tab opens on the sex
+ /// that matches the character's current sex by default.
+ ///
+ private static Sex MapSex(Sex sex)
+ {
+ return sex switch
+ {
+ Sex.Male => Sex.Male,
+ Sex.Female => Sex.Female,
+ Sex.Futanari => Sex.Female,
+ _ => Sex.Male,
+ };
+ }
+
+ public void SetSelectedVoice(string voiceId)
+ {
+ _selectedVoiceId = voiceId;
+ UpdateResults();
+ }
+}
diff --git a/Resources/Locale/en-US/_Art/tts/tts-ui.ftl b/Resources/Locale/en-US/_Art/tts/tts-ui.ftl
new file mode 100644
index 00000000000..f6c9a549727
--- /dev/null
+++ b/Resources/Locale/en-US/_Art/tts/tts-ui.ftl
@@ -0,0 +1,9 @@
+ui-options-tts-volume = TTS volume:
+humanoid-profile-editor-voice-play = ▶
+humanoid-profile-editor-voice-tab = Voice
+humanoid-profile-editor-voice-placeholder = Search voices...
+humanoid-profile-editor-voice-sex-male = Male
+humanoid-profile-editor-voice-sex-female = Female
+humanoid-profile-editor-voice-tooltip-play = Preview
+humanoid-profile-editor-voice-match = Found: {$filtered}/{$all}
+tts-rate-limited = You're generating TTS too fast!
diff --git a/Resources/Locale/ru-RU/_Art/tts/tts-ui.ftl b/Resources/Locale/ru-RU/_Art/tts/tts-ui.ftl
index 555c8ed769b..655cf2d66f3 100644
--- a/Resources/Locale/ru-RU/_Art/tts/tts-ui.ftl
+++ b/Resources/Locale/ru-RU/_Art/tts/tts-ui.ftl
@@ -1,5 +1,8 @@
ui-options-tts-volume = Громкость TTS:
-humanoid-profile-editor-voice-label = ТТС:
-humanoid-profile-editor-voice-play = ▶
-humanoid-profile-editor-tts-enable = Использовать ТТС
+humanoid-profile-editor-voice-tab = Голос
+humanoid-profile-editor-voice-placeholder = Поиск по голосам...
+humanoid-profile-editor-voice-sex-male = Мужские
+humanoid-profile-editor-voice-sex-female = Женские
+humanoid-profile-editor-voice-tooltip-play = Прослушать
+humanoid-profile-editor-voice-match = Найдено: {$filtered}/{$all}
tts-rate-limited = Вы генерируете TTS слишком быстро!
diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml
index 7defcdba0ad..da898c1eef3 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/clone.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/clone.yml
@@ -84,6 +84,7 @@
- LanguageKnowledge
- ForeignerTrait
# Einstein Engines - Language end
+ - TTS # Art
blacklist:
components:
- AttachedClothing # helmets, which are part of the suit