[feat] TTS
# Conflicts: # Content.Client/Options/UI/Tabs/AudioTab.xaml.cs # Content.Client/Preferences/UI/HumanoidProfileEditor.xaml # Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs # Content.Server/Database/ServerDbBase.cs # Content.Server/Entry/EntryPoint.cs # Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs # Content.Server/IoC/ServerContentIoC.cs # Content.Server/VoiceMask/VoiceMaskSystem.cs # Resources/Prototypes/Entities/Mobs/Species/base.yml
This commit is contained in:
@@ -61,6 +61,19 @@
|
|||||||
<Label Name="AmbienceVolumeLabel" MinSize="48 0" Align="Right" />
|
<Label Name="AmbienceVolumeLabel" MinSize="48 0" Align="Right" />
|
||||||
<Control MinSize="4 0"/>
|
<Control MinSize="4 0"/>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
|
||||||
|
<Label Text="{Loc 'ui-options-tts-volume'}" HorizontalExpand="True" />
|
||||||
|
<Control MinSize="8 0" />
|
||||||
|
<Slider Name="TtsVolumeSlider"
|
||||||
|
MinValue="0"
|
||||||
|
MaxValue="200"
|
||||||
|
HorizontalExpand="True"
|
||||||
|
MinSize="80 0"
|
||||||
|
Rounded="True" />
|
||||||
|
<Control MinSize="8 0" />
|
||||||
|
<Label Name="TtsVolumeLabel" MinSize="48 0" Align="Right" />
|
||||||
|
<Control MinSize="4 0"/>
|
||||||
|
</BoxContainer>
|
||||||
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
|
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
|
||||||
<Label Text="{Loc 'ui-options-lobby-volume'}" HorizontalExpand="True" />
|
<Label Text="{Loc 'ui-options-lobby-volume'}" HorizontalExpand="True" />
|
||||||
<Control MinSize="8 0" />
|
<Control MinSize="8 0" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Content.Client.Audio;
|
using Content.Client.Audio;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Shared.White;
|
||||||
using Robust.Client.Audio;
|
using Robust.Client.Audio;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
@@ -36,6 +37,7 @@ namespace Content.Client.Options.UI.Tabs
|
|||||||
AmbienceVolumeSlider.OnValueChanged += OnAmbienceVolumeSliderChanged;
|
AmbienceVolumeSlider.OnValueChanged += OnAmbienceVolumeSliderChanged;
|
||||||
AmbienceSoundsSlider.OnValueChanged += OnAmbienceSoundsSliderChanged;
|
AmbienceSoundsSlider.OnValueChanged += OnAmbienceSoundsSliderChanged;
|
||||||
LobbyVolumeSlider.OnValueChanged += OnLobbyVolumeSliderChanged;
|
LobbyVolumeSlider.OnValueChanged += OnLobbyVolumeSliderChanged;
|
||||||
|
TtsVolumeSlider.OnValueChanged += OnTtsVolumeSliderChanged;
|
||||||
InterfaceVolumeSlider.OnValueChanged += OnInterfaceVolumeSliderChanged;
|
InterfaceVolumeSlider.OnValueChanged += OnInterfaceVolumeSliderChanged;
|
||||||
LobbyMusicCheckBox.OnToggled += OnLobbyMusicCheckToggled;
|
LobbyMusicCheckBox.OnToggled += OnLobbyMusicCheckToggled;
|
||||||
RestartSoundsCheckBox.OnToggled += OnRestartSoundsCheckToggled;
|
RestartSoundsCheckBox.OnToggled += OnRestartSoundsCheckToggled;
|
||||||
@@ -58,9 +60,22 @@ namespace Content.Client.Options.UI.Tabs
|
|||||||
AmbienceVolumeSlider.OnValueChanged -= OnAmbienceVolumeSliderChanged;
|
AmbienceVolumeSlider.OnValueChanged -= OnAmbienceVolumeSliderChanged;
|
||||||
LobbyVolumeSlider.OnValueChanged -= OnLobbyVolumeSliderChanged;
|
LobbyVolumeSlider.OnValueChanged -= OnLobbyVolumeSliderChanged;
|
||||||
InterfaceVolumeSlider.OnValueChanged -= OnInterfaceVolumeSliderChanged;
|
InterfaceVolumeSlider.OnValueChanged -= OnInterfaceVolumeSliderChanged;
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
TtsVolumeSlider.OnValueChanged -= OnTtsVolumeSliderChanged;
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TTS-Start
|
||||||
|
private void OnTtsVolumeSliderChanged(Range obj)
|
||||||
|
{
|
||||||
|
UpdateChanges();
|
||||||
|
}
|
||||||
|
//TTS-End
|
||||||
|
|
||||||
private void OnLobbyVolumeSliderChanged(Range obj)
|
private void OnLobbyVolumeSliderChanged(Range obj)
|
||||||
{
|
{
|
||||||
UpdateChanges();
|
UpdateChanges();
|
||||||
@@ -132,6 +147,11 @@ namespace Content.Client.Options.UI.Tabs
|
|||||||
_cfg.SetCVar(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox.Pressed);
|
_cfg.SetCVar(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox.Pressed);
|
||||||
_cfg.SetCVar(CCVars.EventMusicEnabled, EventMusicCheckBox.Pressed);
|
_cfg.SetCVar(CCVars.EventMusicEnabled, EventMusicCheckBox.Pressed);
|
||||||
_cfg.SetCVar(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox.Pressed);
|
_cfg.SetCVar(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox.Pressed);
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
_cfg.SetCVar(WhiteCVars.TtsVolume, LV100ToDB(TtsVolumeSlider.Value));
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
_cfg.SaveToFile();
|
_cfg.SaveToFile();
|
||||||
UpdateChanges();
|
UpdateChanges();
|
||||||
}
|
}
|
||||||
@@ -156,9 +176,30 @@ namespace Content.Client.Options.UI.Tabs
|
|||||||
RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled);
|
RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled);
|
||||||
EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled);
|
EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled);
|
||||||
AdminSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.AdminSoundsEnabled);
|
AdminSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.AdminSoundsEnabled);
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
TtsVolumeSlider.Value = DBToLV100(_cfg.GetCVar(WhiteCVars.TtsVolume));
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
|
|
||||||
UpdateChanges();
|
UpdateChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Rather than moving these functions somewhere, instead switch MidiManager to using linear units rather than dB
|
||||||
|
// Do be sure to rename the setting though
|
||||||
|
private float DBToLV100(float db, float multiplier = 1f)
|
||||||
|
{
|
||||||
|
var weh = (float) (Math.Pow(10, db / 10) * 100 / multiplier);
|
||||||
|
return weh;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float LV100ToDB(float lv100, float multiplier = 1f)
|
||||||
|
{
|
||||||
|
// Saving negative infinity doesn't work, so use -10000000 instead (MidiManager does it)
|
||||||
|
var weh = MathF.Max(-10000000, (float) (Math.Log(lv100 * multiplier / 100, 10) * 10));
|
||||||
|
return weh;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateChanges()
|
private void UpdateChanges()
|
||||||
{
|
{
|
||||||
// y'all need jesus.
|
// y'all need jesus.
|
||||||
@@ -177,11 +218,18 @@ namespace Content.Client.Options.UI.Tabs
|
|||||||
|
|
||||||
var isAmbientSoundsSame = (int)AmbienceSoundsSlider.Value == _cfg.GetCVar(CCVars.MaxAmbientSources);
|
var isAmbientSoundsSame = (int)AmbienceSoundsSlider.Value == _cfg.GetCVar(CCVars.MaxAmbientSources);
|
||||||
var isLobbySame = LobbyMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.LobbyMusicEnabled);
|
var isLobbySame = LobbyMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.LobbyMusicEnabled);
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
var isTtsVolumeSame =
|
||||||
|
Math.Abs(TtsVolumeSlider.Value - DBToLV100(_cfg.GetCVar(WhiteCVars.TtsVolume))) < 0.01f;
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
var isRestartSoundsSame = RestartSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.RestartSoundsEnabled);
|
var isRestartSoundsSame = RestartSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.RestartSoundsEnabled);
|
||||||
var isEventSame = EventMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.EventMusicEnabled);
|
var isEventSame = EventMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.EventMusicEnabled);
|
||||||
var isAdminSoundsSame = AdminSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.AdminSoundsEnabled);
|
var isAdminSoundsSame = AdminSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.AdminSoundsEnabled);
|
||||||
var isEverythingSame = isMasterVolumeSame && isMidiVolumeSame && isAmbientVolumeSame && isAmbientMusicVolumeSame && isAmbientSoundsSame && isLobbySame && isRestartSoundsSame && isEventSame
|
var isEverythingSame = isMasterVolumeSame && isMidiVolumeSame && isAmbientVolumeSame && isAmbientMusicVolumeSame && isAmbientSoundsSame && isLobbySame && isRestartSoundsSame && isEventSame
|
||||||
&& isAdminSoundsSame && isLobbyVolumeSame && isInterfaceVolumeSame;
|
&& isAdminSoundsSame && isLobbyVolumeSame && isInterfaceVolumeSame;
|
||||||
|
isEverythingSame = isEverythingSame && isTtsVolumeSame; //WD-EDIT
|
||||||
ApplyButton.Disabled = isEverythingSame;
|
ApplyButton.Disabled = isEverythingSame;
|
||||||
ResetButton.Disabled = isEverythingSame;
|
ResetButton.Disabled = isEverythingSame;
|
||||||
MasterVolumeLabel.Text =
|
MasterVolumeLabel.Text =
|
||||||
@@ -197,6 +245,11 @@ namespace Content.Client.Options.UI.Tabs
|
|||||||
InterfaceVolumeLabel.Text =
|
InterfaceVolumeLabel.Text =
|
||||||
Loc.GetString("ui-options-volume-percent", ("volume", InterfaceVolumeSlider.Value / 100));
|
Loc.GetString("ui-options-volume-percent", ("volume", InterfaceVolumeSlider.Value / 100));
|
||||||
AmbienceSoundsLabel.Text = ((int)AmbienceSoundsSlider.Value).ToString();
|
AmbienceSoundsLabel.Text = ((int)AmbienceSoundsSlider.Value).ToString();
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
TtsVolumeLabel.Text =
|
||||||
|
Loc.GetString("ui-options-volume-percent", ("volume", TtsVolumeSlider.Value / 100));
|
||||||
|
//WD-EDIT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,12 @@
|
|||||||
<Control HorizontalExpand="True"/>
|
<Control HorizontalExpand="True"/>
|
||||||
<OptionButton Name="CPronounsButton" HorizontalAlignment="Right" />
|
<OptionButton Name="CPronounsButton" HorizontalAlignment="Right" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
|
<!-- TTS -->
|
||||||
|
<BoxContainer HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'humanoid-profile-editor-voice-label'}" />
|
||||||
|
<Control HorizontalExpand="True"/>
|
||||||
|
<OptionButton Name="CVoiceButton" HorizontalAlignment="Right" />
|
||||||
|
</BoxContainer>
|
||||||
<!-- Show clothing -->
|
<!-- Show clothing -->
|
||||||
<BoxContainer HorizontalExpand="True">
|
<BoxContainer HorizontalExpand="True">
|
||||||
<Label Text="{Loc 'humanoid-profile-editor-clothing'}" />
|
<Label Text="{Loc 'humanoid-profile-editor-clothing'}" />
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ namespace Content.Client.Preferences.UI
|
|||||||
private Button _saveButton => CSaveButton;
|
private Button _saveButton => CSaveButton;
|
||||||
private OptionButton _sexButton => CSexButton;
|
private OptionButton _sexButton => CSexButton;
|
||||||
private OptionButton _genderButton => CPronounsButton;
|
private OptionButton _genderButton => CPronounsButton;
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
private OptionButton _voiceButton => CVoiceButton;
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
private Slider _skinColor => CSkin;
|
private Slider _skinColor => CSkin;
|
||||||
private OptionButton _clothingButton => CClothingButton;
|
private OptionButton _clothingButton => CClothingButton;
|
||||||
private OptionButton _backpackButton => CBackpackButton;
|
private OptionButton _backpackButton => CBackpackButton;
|
||||||
@@ -172,6 +177,14 @@ namespace Content.Client.Preferences.UI
|
|||||||
|
|
||||||
#endregion Gender
|
#endregion Gender
|
||||||
|
|
||||||
|
//TTS-Start
|
||||||
|
#region Voice
|
||||||
|
|
||||||
|
InitializeVoice();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
//TTS-End
|
||||||
|
|
||||||
#region Species
|
#region Species
|
||||||
|
|
||||||
_speciesList = prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart).ToList();
|
_speciesList = prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart).ToList();
|
||||||
@@ -748,9 +761,18 @@ namespace Content.Client.Preferences.UI
|
|||||||
}
|
}
|
||||||
UpdateGenderControls();
|
UpdateGenderControls();
|
||||||
CMarkings.SetSex(newSex);
|
CMarkings.SetSex(newSex);
|
||||||
|
UpdateTTSVoicesControls(); //WD-EDIT
|
||||||
IsDirty = true;
|
IsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
private void SetVoice(string newVoice)
|
||||||
|
{
|
||||||
|
Profile = Profile?.WithVoice(newVoice);
|
||||||
|
IsDirty = true;
|
||||||
|
}
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
private void SetGender(Gender newGender)
|
private void SetGender(Gender newGender)
|
||||||
{
|
{
|
||||||
Profile = Profile?.WithGender(newGender);
|
Profile = Profile?.WithGender(newGender);
|
||||||
@@ -1112,6 +1134,10 @@ namespace Content.Client.Preferences.UI
|
|||||||
UpdateCMarkingsHair();
|
UpdateCMarkingsHair();
|
||||||
UpdateCMarkingsFacialHair();
|
UpdateCMarkingsFacialHair();
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
UpdateTTSVoicesControls();
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
_preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
|
_preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
|
|||||||
|
|
||||||
_window.OpenCentered();
|
_window.OpenCentered();
|
||||||
_window.OnNameChange += OnNameSelected;
|
_window.OnNameChange += OnNameSelected;
|
||||||
|
_window.OnVoiceChange += (value) => SendMessage(new VoiceMaskChangeVoiceMessage(value));
|
||||||
_window.OnClose += Close;
|
_window.OnClose += Close;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_window.UpdateState(cast.Name);
|
_window.UpdateState(cast.Name, cast.Voice);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -7,5 +7,9 @@
|
|||||||
<LineEdit Name="NameSelector" HorizontalExpand="True" />
|
<LineEdit Name="NameSelector" HorizontalExpand="True" />
|
||||||
<Button Name="NameSelectorSet" Text="{Loc 'voice-mask-name-change-set'}" />
|
<Button Name="NameSelectorSet" Text="{Loc 'voice-mask-name-change-set'}" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
|
<Label Text="{Loc 'voice-mask-voice-change-info'}" />
|
||||||
|
<BoxContainer Orientation="Horizontal">
|
||||||
|
<OptionButton Name="VoiceSelector" HorizontalExpand="True" />
|
||||||
|
</BoxContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</DefaultWindow>
|
</DefaultWindow>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Content.Shared.White.TTS;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface.CustomControls;
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Client.VoiceMask;
|
namespace Content.Client.VoiceMask;
|
||||||
|
|
||||||
@@ -9,6 +12,9 @@ public sealed partial class VoiceMaskNameChangeWindow : DefaultWindow
|
|||||||
{
|
{
|
||||||
public Action<string>? OnNameChange;
|
public Action<string>? OnNameChange;
|
||||||
|
|
||||||
|
private readonly List<TTSVoicePrototype> _voices; // TTS
|
||||||
|
public Action<string>? OnVoiceChange;
|
||||||
|
|
||||||
public VoiceMaskNameChangeWindow()
|
public VoiceMaskNameChangeWindow()
|
||||||
{
|
{
|
||||||
RobustXamlLoader.Load(this);
|
RobustXamlLoader.Load(this);
|
||||||
@@ -17,10 +23,31 @@ public sealed partial class VoiceMaskNameChangeWindow : DefaultWindow
|
|||||||
{
|
{
|
||||||
OnNameChange!(NameSelector.Text);
|
OnNameChange!(NameSelector.Text);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
VoiceSelector.OnItemSelected += args =>
|
||||||
|
{
|
||||||
|
VoiceSelector.SelectId(args.Id);
|
||||||
|
if (VoiceSelector.SelectedMetadata != null)
|
||||||
|
OnVoiceChange!((string)VoiceSelector.SelectedMetadata);
|
||||||
|
};
|
||||||
|
_voices = IoCManager
|
||||||
|
.Resolve<IPrototypeManager>()
|
||||||
|
.EnumeratePrototypes<TTSVoicePrototype>()
|
||||||
|
.Where(o => o.RoundStart)
|
||||||
|
.ToList();
|
||||||
|
for (var i = 0; i < _voices.Count; i++)
|
||||||
|
{
|
||||||
|
var name = Loc.GetString(_voices[i].Name);
|
||||||
|
VoiceSelector.AddItem(name);
|
||||||
|
VoiceSelector.SetItemMetadata(i, _voices[i].ID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateState(string name)
|
public void UpdateState(string name, string voice)
|
||||||
{
|
{
|
||||||
NameSelector.Text = name;
|
NameSelector.Text = name;
|
||||||
|
var voiceIdx = _voices.FindIndex(v => v.ID == voice);
|
||||||
|
if (voiceIdx != -1)
|
||||||
|
VoiceSelector.Select(voiceIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
Content.Client/White/TTS/HumanoidProfileEditor.TTS.cs
Normal file
61
Content.Client/White/TTS/HumanoidProfileEditor.TTS.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Content.Client.White.Sponsors;
|
||||||
|
using Content.Shared.Preferences;
|
||||||
|
using Content.Shared.White.TTS;
|
||||||
|
|
||||||
|
namespace Content.Client.Preferences.UI;
|
||||||
|
|
||||||
|
public sealed partial class HumanoidProfileEditor
|
||||||
|
{
|
||||||
|
private List<TTSVoicePrototype> _voiceList = default!;
|
||||||
|
|
||||||
|
private void InitializeVoice()
|
||||||
|
{
|
||||||
|
_voiceList = _prototypeManager.EnumeratePrototypes<TTSVoicePrototype>().Where(o => o.RoundStart).ToList();
|
||||||
|
|
||||||
|
_voiceButton.OnItemSelected += args =>
|
||||||
|
{
|
||||||
|
_voiceButton.SelectId(args.Id);
|
||||||
|
SetVoice(_voiceList[args.Id].ID);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTTSVoicesControls()
|
||||||
|
{
|
||||||
|
if (Profile is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sponsorsManager = IoCManager.Resolve<SponsorsManager>();
|
||||||
|
|
||||||
|
_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;
|
||||||
|
|
||||||
|
if (voice.SponsorOnly &&
|
||||||
|
sponsorsManager.TryGetInfo(out var sponsor) &&
|
||||||
|
!sponsor.AllowedMarkings.Contains(voice.ID))
|
||||||
|
{
|
||||||
|
_voiceButton.SetItemDisabled(i, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice);
|
||||||
|
if (!_voiceButton.TrySelectId(voiceChoiceId) &&
|
||||||
|
_voiceButton.TrySelectId(firstVoiceChoiceId))
|
||||||
|
{
|
||||||
|
SetVoice(_voiceList[firstVoiceChoiceId].ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
213
Content.Client/White/TTS/TTSSystem.cs
Normal file
213
Content.Client/White/TTS/TTSSystem.cs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Shared.White.TTS;
|
||||||
|
using Content.Shared.Physics;
|
||||||
|
using Content.Shared.White;
|
||||||
|
using Robust.Client.Audio;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Shared.Audio.Sources;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Physics;
|
||||||
|
using Robust.Shared.Physics.Systems;
|
||||||
|
|
||||||
|
namespace Content.Client.White.TTS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plays TTS audio in world
|
||||||
|
/// </summary>
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed class TTSSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IAudioManager _audioSystem = default!;
|
||||||
|
[Dependency] private readonly IEntityManager _entity = default!;
|
||||||
|
[Dependency] private readonly IEyeManager _eye = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
[Dependency] private readonly SharedPhysicsSystem _broadPhase = default!;
|
||||||
|
|
||||||
|
private ISawmill _sawmill = default!;
|
||||||
|
private float _volume = 0.0f;
|
||||||
|
|
||||||
|
private readonly HashSet<AudioStream> _currentStreams = new();
|
||||||
|
private readonly Dictionary<EntityUid, Queue<AudioStream>> _entityQueues = new();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
_sawmill = Logger.GetSawmill("tts");
|
||||||
|
_cfg.OnValueChanged(WhiteCVars.TtsVolume, OnTtsVolumeChanged, true);
|
||||||
|
SubscribeNetworkEvent<PlayTTSEvent>(OnPlayTTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
_cfg.UnsubValueChanged(WhiteCVars.TtsVolume, OnTtsVolumeChanged);
|
||||||
|
EndStreams();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Little bit of duplication logic from AudioSystem
|
||||||
|
public override void FrameUpdate(float frameTime)
|
||||||
|
{
|
||||||
|
var streamToRemove = new HashSet<AudioStream>();
|
||||||
|
|
||||||
|
var ourPos = _eye.CurrentEye.Position.Position;
|
||||||
|
foreach (var stream in _currentStreams)
|
||||||
|
{
|
||||||
|
if (!stream.Source.Playing ||
|
||||||
|
!_entity.TryGetComponent<MetaDataComponent>(stream.Uid, out var meta) ||
|
||||||
|
Deleted(stream.Uid, meta) ||
|
||||||
|
!_entity.TryGetComponent<TransformComponent>(stream.Uid, out var xform))
|
||||||
|
{
|
||||||
|
stream.Source.Dispose();
|
||||||
|
streamToRemove.Add(stream);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapPos = xform.MapPosition;
|
||||||
|
if (mapPos.MapId != MapId.Nullspace)
|
||||||
|
{
|
||||||
|
stream.Source.Position = mapPos.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapPos.MapId == _eye.CurrentMap)
|
||||||
|
{
|
||||||
|
var collisionMask = (int) CollisionGroup.Impassable;
|
||||||
|
var sourceRelative = ourPos - mapPos.Position;
|
||||||
|
var occlusion = 0f;
|
||||||
|
if (sourceRelative.Length() > 0)
|
||||||
|
{
|
||||||
|
occlusion = _broadPhase.IntersectRayPenetration(mapPos.MapId,
|
||||||
|
new CollisionRay(mapPos.Position, sourceRelative.Normalized(), collisionMask),
|
||||||
|
sourceRelative.Length(), stream.Uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Source.Occlusion = occlusion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var audioStream in streamToRemove)
|
||||||
|
{
|
||||||
|
_currentStreams.Remove(audioStream);
|
||||||
|
ProcessEntityQueue(audioStream.Uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTtsVolumeChanged(float volume)
|
||||||
|
{
|
||||||
|
_volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayTTS(PlayTTSEvent ev)
|
||||||
|
{
|
||||||
|
if (!TryCreateAudioSource(ev.Data, out var source))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var stream = new AudioStream(GetEntity(ev.Uid), source);
|
||||||
|
AddEntityStreamToQueue(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PlayCustomText(string text)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(new RequestTTSEvent(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryCreateAudioSource(byte[] data, [NotNullWhen(true)] out IAudioSource? source)
|
||||||
|
{
|
||||||
|
var dataStream = new MemoryStream(data) { Position = 0 };
|
||||||
|
var audioStream = _audioSystem.LoadAudioWav(dataStream);
|
||||||
|
source = _audioSystem.CreateAudioSource(audioStream);
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.Volume = _volume;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddEntityStreamToQueue(AudioStream stream)
|
||||||
|
{
|
||||||
|
if (_entityQueues.TryGetValue(stream.Uid, out var queue))
|
||||||
|
{
|
||||||
|
queue.Enqueue(stream);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_entityQueues.Add(stream.Uid, new Queue<AudioStream>(new[] { stream }));
|
||||||
|
|
||||||
|
if (!IsEntityCurrentlyPlayStream(stream.Uid))
|
||||||
|
ProcessEntityQueue(stream.Uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEntityCurrentlyPlayStream(EntityUid uid)
|
||||||
|
{
|
||||||
|
return _currentStreams.Any(s => s.Uid == uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessEntityQueue(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (TryTakeEntityStreamFromQueue(uid, out var stream))
|
||||||
|
PlayEntity(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryTakeEntityStreamFromQueue(EntityUid uid, [NotNullWhen(true)] out AudioStream? stream)
|
||||||
|
{
|
||||||
|
if (_entityQueues.TryGetValue(uid, out var queue))
|
||||||
|
{
|
||||||
|
stream = queue.Dequeue();
|
||||||
|
if (queue.Count == 0)
|
||||||
|
_entityQueues.Remove(uid);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayEntity(AudioStream stream)
|
||||||
|
{
|
||||||
|
if (!_entity.TryGetComponent<TransformComponent>(stream.Uid, out var xform))
|
||||||
|
return;
|
||||||
|
|
||||||
|
stream.Source.Position = xform.WorldPosition;
|
||||||
|
stream.Source.StartPlaying();
|
||||||
|
_currentStreams.Add(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopAllStreams()
|
||||||
|
{
|
||||||
|
foreach (var stream in _currentStreams)
|
||||||
|
{
|
||||||
|
stream.Source.StopPlaying();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndStreams()
|
||||||
|
{
|
||||||
|
foreach (var stream in _currentStreams)
|
||||||
|
{
|
||||||
|
stream.Source.StopPlaying();
|
||||||
|
stream.Source.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentStreams.Clear();
|
||||||
|
_entityQueues.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
private sealed class AudioStream
|
||||||
|
{
|
||||||
|
public EntityUid Uid { get; }
|
||||||
|
|
||||||
|
public IAudioSource Source { get; }
|
||||||
|
|
||||||
|
public AudioStream(EntityUid uid, IAudioSource source)
|
||||||
|
{
|
||||||
|
Uid = uid;
|
||||||
|
Source = source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ namespace Content.IntegrationTests.Tests.Preferences
|
|||||||
"Charlie Charlieson",
|
"Charlie Charlieson",
|
||||||
"The biggest boy around.",
|
"The biggest boy around.",
|
||||||
"Human",
|
"Human",
|
||||||
|
"Eugene",
|
||||||
21,
|
21,
|
||||||
Sex.Male,
|
Sex.Male,
|
||||||
Gender.Epicene,
|
Gender.Epicene,
|
||||||
|
|||||||
1338
Content.Server.Database/Migrations/Postgres/20221214230019_TTSVoice.Designer.cs
generated
Normal file
1338
Content.Server.Database/Migrations/Postgres/20221214230019_TTSVoice.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Content.Server.Database.Migrations.Postgres
|
||||||
|
{
|
||||||
|
public partial class TTSVoice : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "voice",
|
||||||
|
table: "profile",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "voice",
|
||||||
|
table: "profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -810,6 +810,11 @@ namespace Content.Server.Database.Migrations.Postgres
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("species");
|
.HasColumnName("species");
|
||||||
|
|
||||||
|
b.Property<string>("Voice")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("voice");
|
||||||
|
|
||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("PK_profile");
|
.HasName("PK_profile");
|
||||||
|
|
||||||
|
|||||||
1272
Content.Server.Database/Migrations/Sqlite/20221214230014_TTSVoice.Designer.cs
generated
Normal file
1272
Content.Server.Database/Migrations/Sqlite/20221214230014_TTSVoice.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Content.Server.Database.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
public partial class TTSVoice : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "voice",
|
||||||
|
table: "profile",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "voice",
|
||||||
|
table: "profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -764,6 +764,11 @@ namespace Content.Server.Database.Migrations.Sqlite
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("species");
|
.HasColumnName("species");
|
||||||
|
|
||||||
|
b.Property<string>("Voice")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("voice");
|
||||||
|
|
||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("PK_profile");
|
.HasName("PK_profile");
|
||||||
|
|
||||||
|
|||||||
@@ -324,6 +324,11 @@ namespace Content.Server.Database
|
|||||||
public int Age { get; set; }
|
public int Age { get; set; }
|
||||||
public string Sex { get; set; } = null!;
|
public string Sex { get; set; } = null!;
|
||||||
public string Gender { get; set; } = null!;
|
public string Gender { get; set; } = null!;
|
||||||
|
|
||||||
|
//WD-EDIT
|
||||||
|
public string Voice { get; set; } = null!;
|
||||||
|
//WD-EDIT
|
||||||
|
|
||||||
public string Species { get; set; } = null!;
|
public string Species { get; set; } = null!;
|
||||||
[Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!;
|
[Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!;
|
||||||
public string HairName { get; set; } = null!;
|
public string HairName { get; set; } = null!;
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range);
|
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range);
|
||||||
|
|
||||||
var ev = new EntitySpokeEvent(source, message, null, null);
|
var ev = new EntitySpokeEvent(source, message, originalMessage, null, null);
|
||||||
RaiseLocalEvent(source, ev, true);
|
RaiseLocalEvent(source, ev, true);
|
||||||
|
|
||||||
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
|
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
|
||||||
@@ -515,7 +515,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
|
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
|
||||||
|
|
||||||
var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
|
var ev = new EntitySpokeEvent(source, message, originalMessage, channel, obfuscatedMessage);
|
||||||
RaiseLocalEvent(source, ev, true);
|
RaiseLocalEvent(source, ev, true);
|
||||||
if (hideLog)
|
if (hideLog)
|
||||||
return;
|
return;
|
||||||
@@ -940,6 +940,7 @@ public sealed class EntitySpokeEvent : EntityEventArgs
|
|||||||
{
|
{
|
||||||
public readonly EntityUid Source;
|
public readonly EntityUid Source;
|
||||||
public readonly string Message;
|
public readonly string Message;
|
||||||
|
public readonly string OriginalMessage;
|
||||||
public readonly string? ObfuscatedMessage; // not null if this was a whisper
|
public readonly string? ObfuscatedMessage; // not null if this was a whisper
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -948,10 +949,11 @@ public sealed class EntitySpokeEvent : EntityEventArgs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public RadioChannelPrototype? Channel;
|
public RadioChannelPrototype? Channel;
|
||||||
|
|
||||||
public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage)
|
public EntitySpokeEvent(EntityUid source, string message, string originalMessage, RadioChannelPrototype? channel, string? obfuscatedMessage)
|
||||||
{
|
{
|
||||||
Source = source;
|
Source = source;
|
||||||
Message = message;
|
Message = message;
|
||||||
|
OriginalMessage = originalMessage;
|
||||||
Channel = channel;
|
Channel = channel;
|
||||||
ObfuscatedMessage = obfuscatedMessage;
|
ObfuscatedMessage = obfuscatedMessage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ namespace Content.Server.Database
|
|||||||
if (Enum.TryParse<Gender>(profile.Gender, true, out var genderVal))
|
if (Enum.TryParse<Gender>(profile.Gender, true, out var genderVal))
|
||||||
gender = genderVal;
|
gender = genderVal;
|
||||||
|
|
||||||
|
var voice = profile.Voice;
|
||||||
|
if (voice == string.Empty)
|
||||||
|
voice = SharedHumanoidAppearanceSystem.DefaultSexVoice[sex];
|
||||||
|
|
||||||
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
||||||
var markingsRaw = profile.Markings?.Deserialize<List<string>>();
|
var markingsRaw = profile.Markings?.Deserialize<List<string>>();
|
||||||
|
|
||||||
@@ -210,6 +214,7 @@ namespace Content.Server.Database
|
|||||||
profile.CharacterName,
|
profile.CharacterName,
|
||||||
profile.FlavorText,
|
profile.FlavorText,
|
||||||
profile.Species,
|
profile.Species,
|
||||||
|
voice,
|
||||||
profile.Age,
|
profile.Age,
|
||||||
sex,
|
sex,
|
||||||
gender,
|
gender,
|
||||||
@@ -260,6 +265,7 @@ namespace Content.Server.Database
|
|||||||
profile.Markings = markings;
|
profile.Markings = markings;
|
||||||
profile.Slot = slot;
|
profile.Slot = slot;
|
||||||
profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
|
profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
|
||||||
|
profile.Voice = humanoid.Voice;
|
||||||
|
|
||||||
profile.Jobs.Clear();
|
profile.Jobs.Clear();
|
||||||
profile.Jobs.AddRange(
|
profile.Jobs.AddRange(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ using Robust.Shared.Utility;
|
|||||||
using Content.Server.UtkaIntegration;
|
using Content.Server.UtkaIntegration;
|
||||||
using Content.Server.White.JoinQueue;
|
using Content.Server.White.JoinQueue;
|
||||||
using Content.Server.White.Sponsors;
|
using Content.Server.White.Sponsors;
|
||||||
|
using Content.Server.White.TTS;
|
||||||
|
|
||||||
namespace Content.Server.Entry
|
namespace Content.Server.Entry
|
||||||
{
|
{
|
||||||
@@ -109,6 +110,7 @@ namespace Content.Server.Entry
|
|||||||
//WD-EDIT
|
//WD-EDIT
|
||||||
IoCManager.Resolve<SponsorsManager>().Initialize();
|
IoCManager.Resolve<SponsorsManager>().Initialize();
|
||||||
IoCManager.Resolve<JoinQueueManager>().Initialize();
|
IoCManager.Resolve<JoinQueueManager>().Initialize();
|
||||||
|
IoCManager.Resolve<TTSManager>().Initialize();
|
||||||
//WD-EDIT
|
//WD-EDIT
|
||||||
|
|
||||||
_voteManager.Initialize();
|
_voteManager.Initialize();
|
||||||
@@ -147,8 +149,6 @@ namespace Content.Server.Entry
|
|||||||
IoCManager.Resolve<IGameMapManager>().Initialize();
|
IoCManager.Resolve<IGameMapManager>().Initialize();
|
||||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
|
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
|
||||||
IoCManager.Resolve<IBanManager>().Initialize();
|
IoCManager.Resolve<IBanManager>().Initialize();
|
||||||
IoCManager.Resolve<IBqlQueryManager>().DoAutoRegistrations();
|
|
||||||
IoCManager.Resolve<RoleBanManager>().Initialize();
|
|
||||||
|
|
||||||
//WD-EDIT
|
//WD-EDIT
|
||||||
IoCManager.Resolve<UtkaTCPWrapper>().Initialize();
|
IoCManager.Resolve<UtkaTCPWrapper>().Initialize();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Humanoid.Markings;
|
using Content.Shared.Humanoid.Markings;
|
||||||
@@ -26,13 +27,98 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
|
|||||||
|
|
||||||
private void OnExamined(EntityUid uid, HumanoidAppearanceComponent component, ExaminedEvent args)
|
private void OnExamined(EntityUid uid, HumanoidAppearanceComponent component, ExaminedEvent args)
|
||||||
{
|
{
|
||||||
var identity = Identity.Entity(uid, EntityManager);
|
var identity = Identity.Entity(component.Owner, EntityManager);
|
||||||
var species = GetSpeciesRepresentation(component.Species).ToLower();
|
var species = GetSpeciesRepresentation(component.Species).ToLower();
|
||||||
var age = GetAgeRepresentation(component.Species, component.Age);
|
var age = GetAgeRepresentation(component.Species, component.Age);
|
||||||
|
|
||||||
args.PushText(Loc.GetString("humanoid-appearance-component-examine", ("user", identity), ("age", age), ("species", species)));
|
args.PushText(Loc.GetString("humanoid-appearance-component-examine", ("user", identity), ("age", age), ("species", species)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a humanoid character profile directly onto this humanoid mob.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uid">The mob's entity UID.</param>
|
||||||
|
/// <param name="profile">The character profile to load.</param>
|
||||||
|
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||||
|
public void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref humanoid))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSpecies(uid, profile.Species, false, humanoid);
|
||||||
|
SetSex(uid, profile.Sex, false, humanoid);
|
||||||
|
humanoid.EyeColor = profile.Appearance.EyeColor;
|
||||||
|
|
||||||
|
SetSkinColor(uid, profile.Appearance.SkinColor, false);
|
||||||
|
|
||||||
|
humanoid.MarkingSet.Clear();
|
||||||
|
|
||||||
|
// Add markings that doesn't need coloring. We store them until we add all other markings that doesn't need it.
|
||||||
|
var markingFColored = new Dictionary<Marking, MarkingPrototype>();
|
||||||
|
foreach (var marking in profile.Appearance.Markings)
|
||||||
|
{
|
||||||
|
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||||
|
{
|
||||||
|
if (!prototype.ForcedColoring)
|
||||||
|
{
|
||||||
|
AddMarking(uid, marking.MarkingId, marking.MarkingColors, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
markingFColored.Add(marking, prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hair/facial hair - this may eventually be deprecated.
|
||||||
|
// We need to ensure hair before applying it or coloring can try depend on markings that can be invalid
|
||||||
|
var hairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.Hair, out var hairAlpha, _prototypeManager)
|
||||||
|
? profile.Appearance.SkinColor.WithAlpha(hairAlpha) : profile.Appearance.HairColor;
|
||||||
|
var facialHairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.FacialHair, out var facialHairAlpha, _prototypeManager)
|
||||||
|
? profile.Appearance.SkinColor.WithAlpha(facialHairAlpha) : profile.Appearance.FacialHairColor;
|
||||||
|
|
||||||
|
if (_markingManager.Markings.TryGetValue(profile.Appearance.HairStyleId, out var hairPrototype) &&
|
||||||
|
_markingManager.CanBeApplied(profile.Species, profile.Sex, hairPrototype, _prototypeManager))
|
||||||
|
{
|
||||||
|
AddMarking(uid, profile.Appearance.HairStyleId, hairColor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_markingManager.Markings.TryGetValue(profile.Appearance.FacialHairStyleId, out var facialHairPrototype) &&
|
||||||
|
_markingManager.CanBeApplied(profile.Species,profile.Sex, facialHairPrototype, _prototypeManager))
|
||||||
|
{
|
||||||
|
AddMarking(uid, profile.Appearance.FacialHairStyleId, facialHairColor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
humanoid.MarkingSet.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _prototypeManager);
|
||||||
|
|
||||||
|
// Finally adding marking with forced colors
|
||||||
|
foreach (var (marking, prototype) in markingFColored)
|
||||||
|
{
|
||||||
|
var markingColors = MarkingColoring.GetMarkingLayerColors(
|
||||||
|
prototype,
|
||||||
|
profile.Appearance.SkinColor,
|
||||||
|
profile.Appearance.EyeColor,
|
||||||
|
humanoid.MarkingSet
|
||||||
|
);
|
||||||
|
AddMarking(uid, marking.MarkingId, markingColors, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureDefaultMarkings(uid, humanoid);
|
||||||
|
SetTTSVoice(uid, profile.Voice, humanoid);
|
||||||
|
|
||||||
|
humanoid.Gender = profile.Gender;
|
||||||
|
if (TryComp<GrammarComponent>(uid, out var grammar))
|
||||||
|
{
|
||||||
|
grammar.Gender = profile.Gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
humanoid.Age = profile.Age;
|
||||||
|
|
||||||
|
Dirty(humanoid);
|
||||||
|
}
|
||||||
|
|
||||||
// this was done enough times that it only made sense to do it here
|
// this was done enough times that it only made sense to do it here
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -67,6 +153,64 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
|
|||||||
Dirty(targetHumanoid);
|
Dirty(targetHumanoid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a marking to this humanoid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uid">Humanoid mob's UID</param>
|
||||||
|
/// <param name="marking">Marking ID to use</param>
|
||||||
|
/// <param name="color">Color to apply to all marking layers of this marking</param>
|
||||||
|
/// <param name="sync">Whether to immediately sync this marking or not</param>
|
||||||
|
/// <param name="forced">If this marking was forced (ignores marking points)</param>
|
||||||
|
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||||
|
public void AddMarking(EntityUid uid, string marking, Color? color = null, bool sync = true, bool forced = false, HumanoidAppearanceComponent? humanoid = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref humanoid)
|
||||||
|
|| !_markingManager.Markings.TryGetValue(marking, out var prototype))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var markingObject = prototype.AsMarking();
|
||||||
|
markingObject.Forced = forced;
|
||||||
|
if (color != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < prototype.Sprites.Count; i++)
|
||||||
|
{
|
||||||
|
markingObject.SetColor(i, color.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
humanoid.MarkingSet.AddBack(prototype.MarkingCategory, markingObject);
|
||||||
|
|
||||||
|
if (sync)
|
||||||
|
Dirty(humanoid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uid">Humanoid mob's UID</param>
|
||||||
|
/// <param name="marking">Marking ID to use</param>
|
||||||
|
/// <param name="colors">Colors to apply against this marking's set of sprites.</param>
|
||||||
|
/// <param name="sync">Whether to immediately sync this marking or not</param>
|
||||||
|
/// <param name="forced">If this marking was forced (ignores marking points)</param>
|
||||||
|
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||||
|
public void AddMarking(EntityUid uid, string marking, IReadOnlyList<Color> colors, bool sync = true, bool forced = false, HumanoidAppearanceComponent? humanoid = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref humanoid)
|
||||||
|
|| !_markingManager.Markings.TryGetValue(marking, out var prototype))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var markingObject = new Marking(marking, colors);
|
||||||
|
markingObject.Forced = forced;
|
||||||
|
humanoid.MarkingSet.AddBack(prototype.MarkingCategory, markingObject);
|
||||||
|
|
||||||
|
if (sync)
|
||||||
|
Dirty(humanoid);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a marking from a humanoid by ID.
|
/// Removes a marking from a humanoid by ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -202,4 +346,13 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
|
|||||||
|
|
||||||
return Loc.GetString("identity-age-old");
|
return Loc.GetString("identity-age-old");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureDefaultMarkings(EntityUid uid, HumanoidAppearanceComponent? humanoid)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref humanoid))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
humanoid.MarkingSet.EnsureDefault(humanoid.SkinColor, humanoid.EyeColor, _markingManager);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ using Content.Server.Worldgen.Tools;
|
|||||||
using Content.Server.UtkaIntegration;
|
using Content.Server.UtkaIntegration;
|
||||||
using Content.Server.White.JoinQueue;
|
using Content.Server.White.JoinQueue;
|
||||||
using Content.Server.White.Sponsors;
|
using Content.Server.White.Sponsors;
|
||||||
|
using Content.Server.White.TTS;
|
||||||
|
using Content.Shared.Administration;
|
||||||
using Content.Shared.Administration.Logs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Administration.Managers;
|
using Content.Shared.Administration.Managers;
|
||||||
using Content.Shared.Kitchen;
|
using Content.Shared.Kitchen;
|
||||||
@@ -66,6 +68,7 @@ namespace Content.Server.IoC
|
|||||||
IoCManager.Register<SponsorsManager>();
|
IoCManager.Register<SponsorsManager>();
|
||||||
IoCManager.Register<JoinQueueManager>();
|
IoCManager.Register<JoinQueueManager>();
|
||||||
IoCManager.Register<UtkaTCPWrapper>();
|
IoCManager.Register<UtkaTCPWrapper>();
|
||||||
|
IoCManager.Register<TTSManager>();
|
||||||
// WD-EDIT
|
// WD-EDIT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ namespace Content.Server.Sandbox
|
|||||||
//Logger.Info($"{placement.MsgChannel.UserName} spawned {placement.EntityTemplateName} on position {placement.EntityCoordinates}");
|
//Logger.Info($"{placement.MsgChannel.UserName} spawned {placement.EntityTemplateName} on position {placement.EntityCoordinates}");
|
||||||
var data = _playerManager.GetSessionByUserId(placement.MsgChannel.UserId);
|
var data = _playerManager.GetSessionByUserId(placement.MsgChannel.UserId);
|
||||||
var playerUid = data.AttachedEntity.GetValueOrDefault();
|
var playerUid = data.AttachedEntity.GetValueOrDefault();
|
||||||
var coordinates = placement.EntityCoordinates;
|
var coordinates = placement.NetCoordinates;
|
||||||
switch (placement.PlaceType)
|
switch (placement.PlaceType)
|
||||||
{
|
{
|
||||||
case PlacementManagerMessage.StartPlacement:
|
case PlacementManagerMessage.StartPlacement:
|
||||||
@@ -77,7 +77,7 @@ namespace Content.Server.Sandbox
|
|||||||
case PlacementManagerMessage.RequestPlacement:
|
case PlacementManagerMessage.RequestPlacement:
|
||||||
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{placement.EntityTemplateName} was spawned by" +
|
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{placement.EntityTemplateName} was spawned by" +
|
||||||
$" {ToPrettyString(playerUid):player} at " +
|
$" {ToPrettyString(playerUid):player} at " +
|
||||||
$"{ToPrettyString(coordinates.EntityId):entity} X={coordinates.X}, Y={coordinates.Y}");
|
$"{ToPrettyString(coordinates.NetEntity):entity} X={coordinates.X}, Y={coordinates.Y}");
|
||||||
break;
|
break;
|
||||||
case PlacementManagerMessage.RequestEntRemove:
|
case PlacementManagerMessage.RequestEntRemove:
|
||||||
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(placement.EntityUid):entity} was deleted by {ToPrettyString(playerUid):player}");
|
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(placement.EntityUid):entity} was deleted by {ToPrettyString(playerUid):player}");
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public sealed class UtkaStatusCommand : IUtkaCommand
|
|||||||
|
|
||||||
string? gameMap = null;
|
string? gameMap = null;
|
||||||
string? stationCode = null;
|
string? stationCode = null;
|
||||||
foreach (var station in _station.Stations)
|
foreach (var station in _station.GetStations())
|
||||||
{
|
{
|
||||||
if (!_entMan.TryGetComponent(station, out AlertLevelComponent? alert) || stationCode != null)
|
if (!_entMan.TryGetComponent(station, out AlertLevelComponent? alert) || stationCode != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Content.Shared.Humanoid;
|
||||||
|
|
||||||
namespace Content.Server.VoiceMask;
|
namespace Content.Server.VoiceMask;
|
||||||
|
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
@@ -6,4 +8,7 @@ public sealed partial class VoiceMaskComponent : Component
|
|||||||
[ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true;
|
[ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true;
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)] public string VoiceName = "Unknown";
|
[ViewVariables(VVAccess.ReadWrite)] public string VoiceName = "Unknown";
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)] public string VoiceId = SharedHumanoidAppearanceSystem.DefaultVoice; //TTS
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public sealed partial class VoiceMaskSystem
|
|||||||
|
|
||||||
var comp = EnsureComp<VoiceMaskComponent>(user);
|
var comp = EnsureComp<VoiceMaskComponent>(user);
|
||||||
comp.VoiceName = component.LastSetName;
|
comp.VoiceName = component.LastSetName;
|
||||||
|
if (component.LastSetVoice != null)
|
||||||
|
comp.VoiceId = component.LastSetVoice;
|
||||||
|
|
||||||
_actions.AddAction(user, ref component.ActionEntity, component.Action, uid);
|
_actions.AddAction(user, ref component.ActionEntity, component.Action, uid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem
|
|||||||
SubscribeLocalEvent<VoiceMaskerComponent, GotUnequippedEvent>(OnUnequip);
|
SubscribeLocalEvent<VoiceMaskerComponent, GotUnequippedEvent>(OnUnequip);
|
||||||
SubscribeLocalEvent<VoiceMaskSetNameEvent>(OnSetName);
|
SubscribeLocalEvent<VoiceMaskSetNameEvent>(OnSetName);
|
||||||
// SubscribeLocalEvent<VoiceMaskerComponent, GetVerbsEvent<AlternativeVerb>>(GetVerbs);
|
// SubscribeLocalEvent<VoiceMaskerComponent, GetVerbsEvent<AlternativeVerb>>(GetVerbs);
|
||||||
|
InitializeTTS();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSetName(VoiceMaskSetNameEvent ev)
|
private void OnSetName(VoiceMaskSetNameEvent ev)
|
||||||
@@ -93,6 +94,6 @@ public sealed partial class VoiceMaskSystem : EntitySystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_uiSystem.TryGetUi(owner, VoiceMaskUIKey.Key, out var bui))
|
if (_uiSystem.TryGetUi(owner, VoiceMaskUIKey.Key, out var bui))
|
||||||
_uiSystem.SetUiState(bui, new VoiceMaskBuiState(component.VoiceName));
|
_uiSystem.SetUiState(bui, new VoiceMaskBuiState(component.VoiceName, component.VoiceId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Content.Server.VoiceMask;
|
|||||||
public sealed partial class VoiceMaskerComponent : Component
|
public sealed partial class VoiceMaskerComponent : Component
|
||||||
{
|
{
|
||||||
[ViewVariables(VVAccess.ReadWrite)] public string LastSetName = "Unknown";
|
[ViewVariables(VVAccess.ReadWrite)] public string LastSetName = "Unknown";
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)] public string? LastSetVoice; // tts
|
||||||
|
|
||||||
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
public string Action = "ActionChangeVoiceMask";
|
public string Action = "ActionChangeVoiceMask";
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Server.Administration;
|
|
||||||
using Content.Shared.Administration;
|
|
||||||
using Robust.Shared.Console;
|
|
||||||
|
|
||||||
namespace Content.Client.Commands;
|
|
||||||
|
|
||||||
[AdminCommand(AdminFlags.Debug)]
|
|
||||||
public sealed class SetGlobalZoomCommand : IConsoleCommand
|
|
||||||
{
|
|
||||||
public string Command => "setglobalzoom";
|
|
||||||
public string Description => "Sets the global zoom for all characters.";
|
|
||||||
public string Help => "setglobalzoom <float>";
|
|
||||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
|
||||||
{
|
|
||||||
if(args.Length != 1)
|
|
||||||
{
|
|
||||||
shell.WriteLine("Invalid number of arguments.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!float.TryParse(args[0], out var zoom))
|
|
||||||
{
|
|
||||||
shell.WriteLine("Invalid zoom value.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var entityManager = IoCManager.Resolve<EntityManager>();
|
|
||||||
var eyes = entityManager.GetAllComponents(typeof(SharedEyeComponent), true).Cast<SharedEyeComponent>().ToList();
|
|
||||||
|
|
||||||
foreach (var eye in eyes)
|
|
||||||
{
|
|
||||||
eye.Zoom = new Vector2(zoom, zoom);
|
|
||||||
entityManager.Dirty(eye);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
Content.Server/White/TTS/HumanoidSystemTTS.cs
Normal file
17
Content.Server/White/TTS/HumanoidSystemTTS.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Content.Server.White.TTS;
|
||||||
|
using Content.Shared.Humanoid;
|
||||||
|
|
||||||
|
namespace Content.Server.Humanoid;
|
||||||
|
|
||||||
|
public sealed partial class HumanoidAppearanceSystem
|
||||||
|
{
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public void SetTTSVoice(EntityUid uid, string voiceId, HumanoidAppearanceComponent humanoid)
|
||||||
|
{
|
||||||
|
if (!TryComp<TTSComponent>(uid, out var comp))
|
||||||
|
return;
|
||||||
|
|
||||||
|
humanoid.Voice = voiceId;
|
||||||
|
comp.VoicePrototypeId = voiceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Content.Server/White/TTS/TTSComponent.cs
Normal file
19
Content.Server/White/TTS/TTSComponent.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Content.Shared.White.TTS;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
|
namespace Content.Server.White.TTS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply TTS for entity chat say messages
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed partial class TTSComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prototype of used voice for TTS.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("voice", customTypeSerializer:typeof(PrototypeIdSerializer<TTSVoicePrototype>))]
|
||||||
|
public string VoicePrototypeId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
177
Content.Server/White/TTS/TTSManager.cs
Normal file
177
Content.Server/White/TTS/TTSManager.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Shared.White;
|
||||||
|
using Prometheus;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
|
||||||
|
namespace Content.Server.White.TTS;
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed class TTSManager
|
||||||
|
{
|
||||||
|
private static readonly Histogram RequestTimings = Metrics.CreateHistogram(
|
||||||
|
"tts_req_timings",
|
||||||
|
"Timings of TTS API requests",
|
||||||
|
new HistogramConfiguration()
|
||||||
|
{
|
||||||
|
LabelNames = new[] {"type"},
|
||||||
|
Buckets = Histogram.ExponentialBuckets(.1, 1.5, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
private static readonly Counter WantedCount = Metrics.CreateCounter(
|
||||||
|
"tts_wanted_count",
|
||||||
|
"Amount of wanted TTS audio.");
|
||||||
|
|
||||||
|
private static readonly Counter ReusedCount = Metrics.CreateCounter(
|
||||||
|
"tts_reused_count",
|
||||||
|
"Amount of reused TTS audio from cache.");
|
||||||
|
|
||||||
|
private static readonly Gauge CachedCount = Metrics.CreateGauge(
|
||||||
|
"tts_cached_count",
|
||||||
|
"Amount of cached TTS audio.");
|
||||||
|
|
||||||
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
|
||||||
|
private readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
|
private ISawmill _sawmill = default!;
|
||||||
|
private readonly Dictionary<string, byte[]> _cache = new();
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_sawmill = Logger.GetSawmill("tts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates audio with passed text by API
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="speaker">Identifier of speaker</param>
|
||||||
|
/// <param name="text">SSML formatted text</param>
|
||||||
|
/// <returns>OGG audio bytes</returns>
|
||||||
|
/// <exception cref="Exception">Throws if url or token CCVar not set or http request failed</exception>
|
||||||
|
public async Task<byte[]> ConvertTextToSpeech(string entityName, string speaker, string text)
|
||||||
|
{
|
||||||
|
var url = _cfg.GetCVar(WhiteCVars.TTSApiUrl);
|
||||||
|
var maxCacheSize = _cfg.GetCVar(WhiteCVars.TTSMaxCacheSize);
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
throw new Exception("TTS Api url not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
WantedCount.Inc();
|
||||||
|
var cacheKey = GenerateCacheKey(speaker, text);
|
||||||
|
if (_cache.TryGetValue(cacheKey, out var data))
|
||||||
|
{
|
||||||
|
ReusedCount.Inc();
|
||||||
|
_sawmill.Debug($"Use cached sound for '{text}' speech by '{speaker}' speaker");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = new GenerateVoiceRequest
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Speaker = speaker,
|
||||||
|
Ckey = entityName,
|
||||||
|
};
|
||||||
|
|
||||||
|
var request = CreateRequestLink(url, body);
|
||||||
|
|
||||||
|
var reqTime = DateTime.UtcNow;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||||
|
var response = await _httpClient.GetAsync(request, cts.Token);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception($"TTS request returned bad status code: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var soundData = await response.Content.ReadAsByteArrayAsync(cts.Token);
|
||||||
|
|
||||||
|
if(_cache.Count > maxCacheSize)
|
||||||
|
{
|
||||||
|
_cache.Remove(_cache.Last().Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache.Add(cacheKey, soundData);
|
||||||
|
CachedCount.Inc();
|
||||||
|
|
||||||
|
_sawmill.Debug($"Generated new sound for '{text}' speech by '{speaker}' speaker ({soundData.Length} bytes)");
|
||||||
|
RequestTimings.WithLabels("Success").Observe((DateTime.UtcNow - reqTime).TotalSeconds);
|
||||||
|
|
||||||
|
return soundData;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
RequestTimings.WithLabels("Timeout").Observe((DateTime.UtcNow - reqTime).TotalSeconds);
|
||||||
|
_sawmill.Error($"Timeout of request generation new sound for '{text}' speech by '{speaker}' speaker");
|
||||||
|
throw new Exception("TTS request timeout");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RequestTimings.WithLabels("Error").Observe((DateTime.UtcNow - reqTime).TotalSeconds);
|
||||||
|
_sawmill.Error($"Failed of request generation new sound for '{text}' speech by '{speaker}' speaker\n{e}");
|
||||||
|
throw new Exception("TTS request failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateRequestLink(string url, GenerateVoiceRequest body)
|
||||||
|
{
|
||||||
|
var uriBuilder = new UriBuilder(url);
|
||||||
|
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||||
|
query["ckey"] = body.Ckey;
|
||||||
|
query["speaker"] = body.Speaker;
|
||||||
|
query["text"] = body.Text;
|
||||||
|
query["file"] = "1";
|
||||||
|
uriBuilder.Query = query.ToString();
|
||||||
|
return uriBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetCache()
|
||||||
|
{
|
||||||
|
_cache.Clear();
|
||||||
|
CachedCount.Set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateCacheKey(string speaker, string text)
|
||||||
|
{
|
||||||
|
var key = $"{speaker}/{text}";
|
||||||
|
byte[] keyData = Encoding.UTF8.GetBytes(key);
|
||||||
|
var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||||
|
var bytes = sha256.ComputeHash(keyData);
|
||||||
|
return Convert.ToHexString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record GenerateVoiceRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("text")]
|
||||||
|
public string Text { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("speaker")]
|
||||||
|
public string Speaker { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("ckey")]
|
||||||
|
public string Ckey { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct GenerateVoiceResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("results")]
|
||||||
|
public List<VoiceResult> Results { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("original_sha1")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct VoiceResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("audio")]
|
||||||
|
public string Audio { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Content.Server/White/TTS/TTSSystem.SSML.cs
Normal file
29
Content.Server/White/TTS/TTSSystem.SSML.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace Content.Server.White.TTS;
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed partial class TTSSystem
|
||||||
|
{
|
||||||
|
private string ToSsmlText(string text, SpeechRate rate = SpeechRate.Medium)
|
||||||
|
{
|
||||||
|
return $"<speak><prosody rate=\"{SpeechRateMap[rate]}\">{text}</prosody></speak>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SpeechRate : byte
|
||||||
|
{
|
||||||
|
VerySlow,
|
||||||
|
Slow,
|
||||||
|
Medium,
|
||||||
|
Fast,
|
||||||
|
VeryFast,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<SpeechRate, string> SpeechRateMap =
|
||||||
|
new Dictionary<SpeechRate, string>()
|
||||||
|
{
|
||||||
|
{SpeechRate.VerySlow, "x-slow"},
|
||||||
|
{SpeechRate.Slow, "slow"},
|
||||||
|
{SpeechRate.Medium, "medium"},
|
||||||
|
{SpeechRate.Fast, "fast"},
|
||||||
|
{SpeechRate.VeryFast, "x-fast"},
|
||||||
|
};
|
||||||
|
}
|
||||||
288
Content.Server/White/TTS/TTSSystem.Sanitize.cs
Normal file
288
Content.Server/White/TTS/TTSSystem.Sanitize.cs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Content.Server.Chat.Systems;
|
||||||
|
|
||||||
|
namespace Content.Server.White.TTS;
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed partial class TTSSystem
|
||||||
|
{
|
||||||
|
private void OnTransformSpeech(TransformSpeechEvent args)
|
||||||
|
{
|
||||||
|
if (!_isEnabled)
|
||||||
|
return;
|
||||||
|
args.Message = args.Message.Replace("+", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Sanitize(string text)
|
||||||
|
{
|
||||||
|
text = text.Trim();
|
||||||
|
text = Regex.Replace(text, @"Ё", "Е");
|
||||||
|
text = Regex.Replace(text, @"[^a-zA-Zа-яА-ЯёЁ0-9,\-,+, ,?,!]", "");
|
||||||
|
text = Regex.Replace(text, @"[a-zA-Z]", ReplaceLat2Cyr, RegexOptions.Multiline | RegexOptions.IgnoreCase);
|
||||||
|
text = Regex.Replace(text, @"(?<![a-zA-Zа-яёА-ЯЁ])[a-zA-Zа-яёА-ЯЁ]+?(?![a-zA-Zа-яёА-ЯЁ])", ReplaceMatchedWord, RegexOptions.Multiline | RegexOptions.IgnoreCase);
|
||||||
|
text = Regex.Replace(text, @"(?<=[1-90])(\.|,)(?=[1-90])", " целых ");
|
||||||
|
text = Regex.Replace(text, @"\d+", ReplaceWord2Num);
|
||||||
|
text = text.Trim();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceLat2Cyr(Match oneChar)
|
||||||
|
{
|
||||||
|
if (ReverseTranslit.TryGetValue(oneChar.Value.ToLower(), out var replace))
|
||||||
|
return replace;
|
||||||
|
return oneChar.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceMatchedWord(Match word)
|
||||||
|
{
|
||||||
|
if (WordReplacement.TryGetValue(word.Value.ToLower(), out var replace))
|
||||||
|
return replace;
|
||||||
|
return word.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceWord2Num(Match word)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(word.Value, out var number))
|
||||||
|
return word.Value;
|
||||||
|
return NumberConverter.NumberToText(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> WordReplacement =
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"нт", "Эн Тэ"},
|
||||||
|
{"смо", "Эс Мэ О"},
|
||||||
|
{"гп", "Гэ Пэ"},
|
||||||
|
{"рд", "Эр Дэ"},
|
||||||
|
{"гсб", "Гэ Эс Бэ"},
|
||||||
|
{"гв", "Гэ Вэ"},
|
||||||
|
{"нр", "Эн Эр"},
|
||||||
|
{"срп", "Эс Эр Пэ"},
|
||||||
|
{"цк", "Цэ Каа"},
|
||||||
|
{"рнд", "Эр Эн Дэ"},
|
||||||
|
{"сб", "Эс Бэ"},
|
||||||
|
{"рцд", "Эр Цэ Дэ"},
|
||||||
|
{"брпд", "Бэ Эр Пэ Дэ"},
|
||||||
|
{"рпд", "Эр Пэ Дэ"},
|
||||||
|
{"рпед", "Эр Пед"},
|
||||||
|
{"тсф", "Тэ Эс Эф"},
|
||||||
|
{"срт", "Эс Эр Тэ"},
|
||||||
|
{"обр", "О Бэ Эр"},
|
||||||
|
{"кпк", "Кэ Пэ Каа"},
|
||||||
|
{"пда", "Пэ Дэ А"},
|
||||||
|
{"id", "Ай Ди"},
|
||||||
|
{"мщ", "Эм Ще"},
|
||||||
|
{"вт", "Вэ Тэ"},
|
||||||
|
{"ерп", "Йе Эр Пэ"},
|
||||||
|
{"се", "Эс Йе"},
|
||||||
|
{"апц", "А Пэ Цэ"},
|
||||||
|
{"лкп", "Эл Ка Пэ"},
|
||||||
|
{"см", "Эс Эм"},
|
||||||
|
{"ека", "Йе Ка"},
|
||||||
|
{"ка", "Кэ А"},
|
||||||
|
{"бса", "Бэ Эс Аа"},
|
||||||
|
{"тк", "Тэ Ка"},
|
||||||
|
{"бфл", "Бэ Эф Эл"},
|
||||||
|
{"бщ", "Бэ Щэ"},
|
||||||
|
{"кк", "Кэ Ка"},
|
||||||
|
{"ск", "Эс Ка"},
|
||||||
|
{"зк", "Зэ Ка"},
|
||||||
|
{"ерт", "Йе Эр Тэ"},
|
||||||
|
{"вкд", "Вэ Ка Дэ"},
|
||||||
|
{"нтр", "Эн Тэ Эр"},
|
||||||
|
{"пнт", "Пэ Эн Тэ"},
|
||||||
|
{"авд", "А Вэ Дэ"},
|
||||||
|
{"пнв", "Пэ Эн Вэ"},
|
||||||
|
{"ссд", "Эс Эс Дэ"},
|
||||||
|
{"кпб", "Кэ Пэ Бэ"},
|
||||||
|
{"сссп", "Эс Эс Эс Пэ"},
|
||||||
|
{"крб", "Ка Эр Бэ"},
|
||||||
|
{"бд", "Бэ Дэ"},
|
||||||
|
{"сст", "Эс Эс Тэ"},
|
||||||
|
{"скс", "Эс Ка Эс"},
|
||||||
|
{"икн", "И Ка Эн"},
|
||||||
|
{"нсс", "Эн Эс Эс"},
|
||||||
|
{"емп", "Йе Эм Пэ"},
|
||||||
|
{"бс", "Бэ Эс"},
|
||||||
|
{"цкс", "Цэ Ка Эс"},
|
||||||
|
{"срд", "Эс Эр Дэ"},
|
||||||
|
{"жпс", "Джи Пи Эс"},
|
||||||
|
{"gps", "Джи Пи Эс"},
|
||||||
|
{"ннксс", "Эн Эн Ка Эс Эс"},
|
||||||
|
{"ss", "Эс Эс"},
|
||||||
|
{"сс", "Эс Эс"},
|
||||||
|
{"тесла", "тэсла"},
|
||||||
|
{"трейзен", "трэйзэн"},
|
||||||
|
{"нанотрейзен", "нанотрэйзэн"},
|
||||||
|
{"рпзд", "Эр Пэ Зэ Дэ"},
|
||||||
|
{"кз", "Кэ Зэ"},
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> ReverseTranslit =
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"a", "а"},
|
||||||
|
{"b", "б"},
|
||||||
|
{"v", "в"},
|
||||||
|
{"g", "г"},
|
||||||
|
{"d", "д"},
|
||||||
|
{"e", "е"},
|
||||||
|
{"je", "ё"},
|
||||||
|
{"zh", "ж"},
|
||||||
|
{"z", "з"},
|
||||||
|
{"i", "и"},
|
||||||
|
{"y", "й"},
|
||||||
|
{"k", "к"},
|
||||||
|
{"l", "л"},
|
||||||
|
{"m", "м"},
|
||||||
|
{"n", "н"},
|
||||||
|
{"o", "о"},
|
||||||
|
{"p", "п"},
|
||||||
|
{"r", "р"},
|
||||||
|
{"s", "с"},
|
||||||
|
{"t", "т"},
|
||||||
|
{"u", "у"},
|
||||||
|
{"f", "ф"},
|
||||||
|
{"h", "х"},
|
||||||
|
{"c", "ц"},
|
||||||
|
{"x", "кс"},
|
||||||
|
{"ch", "ч"},
|
||||||
|
{"sh", "ш"},
|
||||||
|
{"jsh", "щ"},
|
||||||
|
{"hh", "ъ"},
|
||||||
|
{"ih", "ы"},
|
||||||
|
{"jh", "ь"},
|
||||||
|
{"eh", "э"},
|
||||||
|
{"ju", "ю"},
|
||||||
|
{"ja", "я"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source: https://codelab.ru/s/csharp/digits2phrase
|
||||||
|
public static class NumberConverter
|
||||||
|
{
|
||||||
|
private static readonly string[] Frac20Male =
|
||||||
|
{
|
||||||
|
"", "один", "два", "три", "четыре", "пять", "шесть",
|
||||||
|
"семь", "восемь", "девять", "десять", "одиннадцать",
|
||||||
|
"двенадцать", "тринадцать", "четырнадцать", "пятнадцать",
|
||||||
|
"шестнадцать", "семнадцать", "восемнадцать", "девятнадцать"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] Frac20Female =
|
||||||
|
{
|
||||||
|
"", "одна", "две", "три", "четыре", "пять", "шесть",
|
||||||
|
"семь", "восемь", "девять", "десять", "одиннадцать",
|
||||||
|
"двенадцать", "тринадцать", "четырнадцать", "пятнадцать",
|
||||||
|
"шестнадцать", "семнадцать", "восемнадцать", "девятнадцать"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] Hunds =
|
||||||
|
{
|
||||||
|
"", "сто", "двести", "триста", "четыреста",
|
||||||
|
"пятьсот", "шестьсот", "семьсот", "восемьсот", "девятьсот"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] Tens =
|
||||||
|
{
|
||||||
|
"", "десять", "двадцать", "тридцать", "сорок", "пятьдесят",
|
||||||
|
"шестьдесят", "семьдесят", "восемьдесят", "девяносто"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string NumberToText(long value, bool male = true)
|
||||||
|
{
|
||||||
|
if (value >= (long)Math.Pow(10, 15))
|
||||||
|
return String.Empty;
|
||||||
|
|
||||||
|
if (value == 0)
|
||||||
|
return "ноль";
|
||||||
|
|
||||||
|
var str = new StringBuilder();
|
||||||
|
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
str.Append("минус");
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = AppendPeriod(value, 1000000000000, str, "триллион", "триллиона", "триллионов", true);
|
||||||
|
value = AppendPeriod(value, 1000000000, str, "миллиард", "миллиарда", "миллиардов", true);
|
||||||
|
value = AppendPeriod(value, 1000000, str, "миллион", "миллиона", "миллионов", true);
|
||||||
|
value = AppendPeriod(value, 1000, str, "тысяча", "тысячи", "тысяч", false);
|
||||||
|
|
||||||
|
var hundreds = (int)(value / 100);
|
||||||
|
if (hundreds != 0)
|
||||||
|
AppendWithSpace(str, Hunds[hundreds]);
|
||||||
|
|
||||||
|
var less100 = (int)(value % 100);
|
||||||
|
var frac20 = male ? Frac20Male : Frac20Female;
|
||||||
|
if (less100 < 20)
|
||||||
|
AppendWithSpace(str, frac20[less100]);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var tens = less100 / 10;
|
||||||
|
AppendWithSpace(str, Tens[tens]);
|
||||||
|
var less10 = less100 % 10;
|
||||||
|
if (less10 != 0)
|
||||||
|
str.Append(" " + frac20[less100%10]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendWithSpace(StringBuilder stringBuilder, string str)
|
||||||
|
{
|
||||||
|
if (stringBuilder.Length > 0)
|
||||||
|
stringBuilder.Append(" ");
|
||||||
|
stringBuilder.Append(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long AppendPeriod(
|
||||||
|
long value,
|
||||||
|
long power,
|
||||||
|
StringBuilder str,
|
||||||
|
string declension1,
|
||||||
|
string declension2,
|
||||||
|
string declension5,
|
||||||
|
bool male)
|
||||||
|
{
|
||||||
|
var thousands = (int)(value / power);
|
||||||
|
if (thousands > 0)
|
||||||
|
{
|
||||||
|
AppendWithSpace(str, NumberToText(thousands, male, declension1, declension2, declension5));
|
||||||
|
return value % power;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NumberToText(
|
||||||
|
long value,
|
||||||
|
bool male,
|
||||||
|
string valueDeclensionFor1,
|
||||||
|
string valueDeclensionFor2,
|
||||||
|
string valueDeclensionFor5)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
NumberToText(value, male)
|
||||||
|
+ " "
|
||||||
|
+ GetDeclension((int)(value % 10), valueDeclensionFor1, valueDeclensionFor2, valueDeclensionFor5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDeclension(int val, string one, string two, string five)
|
||||||
|
{
|
||||||
|
var t = (val % 100 > 20) ? val % 10 : val % 20;
|
||||||
|
|
||||||
|
switch (t)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
return one;
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
return two;
|
||||||
|
default:
|
||||||
|
return five;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
Content.Server/White/TTS/TTSSystem.cs
Normal file
132
Content.Server/White/TTS/TTSSystem.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Shared.White.TTS;
|
||||||
|
using Content.Shared.GameTicking;
|
||||||
|
using Content.Shared.White;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.White.TTS;
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed partial class TTSSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly TTSManager _ttsManager = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
|
||||||
|
private const int MaxMessageChars = 100 * 2; // same as SingleBubbleCharLimit * 2
|
||||||
|
private bool _isEnabled = false;
|
||||||
|
private string _apiUrl = string.Empty;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
_cfg.OnValueChanged(WhiteCVars.TTSEnabled, v => _isEnabled = v, true);
|
||||||
|
_cfg.OnValueChanged(WhiteCVars.TTSApiUrl, url => _apiUrl = url, true);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<TransformSpeechEvent>(OnTransformSpeech);
|
||||||
|
SubscribeLocalEvent<TTSComponent, EntitySpokeEvent>(OnEntitySpoke);
|
||||||
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
||||||
|
SubscribeNetworkEvent<RequestTTSEvent>(OnRequestTTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRequestTTS(RequestTTSEvent ev)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnEntitySpoke(EntityUid uid, TTSComponent component, EntitySpokeEvent args)
|
||||||
|
{
|
||||||
|
if (!_isEnabled ||
|
||||||
|
args.Message.Length > MaxMessageChars)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_apiUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var voiceId = component.VoicePrototypeId;
|
||||||
|
var voiceEv = new TransformSpeakerVoiceEvent(uid, voiceId);
|
||||||
|
RaiseLocalEvent(uid, voiceEv);
|
||||||
|
voiceId = voiceEv.VoiceId;
|
||||||
|
|
||||||
|
if (!_prototypeManager.TryIndex<TTSVoicePrototype>(voiceId, out var protoVoice))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var message = FormattedMessage.RemoveMarkup(args.Message);
|
||||||
|
|
||||||
|
var soundData = await GenerateTTS(uid, message, protoVoice.Speaker);
|
||||||
|
if (soundData is null)
|
||||||
|
return;
|
||||||
|
var ttsEvent = new PlayTTSEvent(GetNetEntity(uid), soundData);
|
||||||
|
|
||||||
|
// Say
|
||||||
|
if (args.ObfuscatedMessage is null)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(ttsEvent, Filter.Pvs(uid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whisper
|
||||||
|
var wList = new List<string>
|
||||||
|
{
|
||||||
|
"тсс",
|
||||||
|
"псс",
|
||||||
|
"ччч",
|
||||||
|
"ссч",
|
||||||
|
"сфч",
|
||||||
|
"тст"
|
||||||
|
};
|
||||||
|
var chosenWhisperText = _random.Pick(wList);
|
||||||
|
var obfSoundData = await GenerateTTS(uid, chosenWhisperText, protoVoice.Speaker);
|
||||||
|
if (obfSoundData is null)
|
||||||
|
return;
|
||||||
|
var obfTtsEvent = new PlayTTSEvent(GetNetEntity(uid), obfSoundData);
|
||||||
|
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||||
|
var sourcePos = _xforms.GetWorldPosition(xformQuery.GetComponent(uid), xformQuery);
|
||||||
|
var receptions = Filter.Pvs(uid).Recipients;
|
||||||
|
foreach (var session in receptions)
|
||||||
|
{
|
||||||
|
if (!session.AttachedEntity.HasValue)
|
||||||
|
continue;
|
||||||
|
var xform = xformQuery.GetComponent(session.AttachedEntity.Value);
|
||||||
|
var distance = (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).LengthSquared();
|
||||||
|
if (distance > (ChatSystem.VoiceRange * ChatSystem.VoiceRange))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
RaiseNetworkEvent(distance > ChatSystem.WhisperClearRange ? obfTtsEvent : ttsEvent, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
|
||||||
|
{
|
||||||
|
_ttsManager.ResetCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]?> GenerateTTS(EntityUid uid, string text, string speaker)
|
||||||
|
{
|
||||||
|
var textSanitized = Sanitize(text);
|
||||||
|
if (textSanitized == "")
|
||||||
|
return null;
|
||||||
|
var metadata = Comp<MetaDataComponent>(uid);
|
||||||
|
return await _ttsManager.ConvertTextToSpeech(metadata.EntityName, speaker, textSanitized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TransformSpeakerVoiceEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid Sender;
|
||||||
|
public string VoiceId;
|
||||||
|
|
||||||
|
public TransformSpeakerVoiceEvent(EntityUid sender, string voiceId)
|
||||||
|
{
|
||||||
|
Sender = sender;
|
||||||
|
VoiceId = voiceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Content.Server/White/TTS/VoiceMaskSystem.cs
Normal file
42
Content.Server/White/TTS/VoiceMaskSystem.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Content.Server.White.TTS;
|
||||||
|
using Content.Shared.VoiceMask;
|
||||||
|
|
||||||
|
namespace Content.Server.VoiceMask;
|
||||||
|
|
||||||
|
public partial class VoiceMaskSystem
|
||||||
|
{
|
||||||
|
private void InitializeTTS()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<VoiceMaskComponent, TransformSpeakerVoiceEvent>(OnSpeakerVoiceTransform);
|
||||||
|
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVoiceMessage>(OnChangeVoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpeakerVoiceTransform(EntityUid uid, VoiceMaskComponent component, TransformSpeakerVoiceEvent args)
|
||||||
|
{
|
||||||
|
if (component.Enabled)
|
||||||
|
args.VoiceId = component.VoiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChangeVoice(EntityUid uid, VoiceMaskComponent component, VoiceMaskChangeVoiceMessage message)
|
||||||
|
{
|
||||||
|
component.VoiceId = message.Voice;
|
||||||
|
|
||||||
|
_popupSystem.PopupCursor(Loc.GetString("voice-mask-voice-popup-success"), message.Session);
|
||||||
|
|
||||||
|
TrySetLastKnownVoice(uid, message.Voice);
|
||||||
|
|
||||||
|
UpdateUI(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrySetLastKnownVoice(EntityUid maskWearer, string? voiceId)
|
||||||
|
{
|
||||||
|
if (!HasComp<VoiceMaskComponent>(maskWearer)
|
||||||
|
|| !_inventory.TryGetSlotEntity(maskWearer, MaskSlot, out var maskEntity)
|
||||||
|
|| !TryComp<VoiceMaskerComponent>(maskEntity, out var maskComp))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
maskComp.LastSetVoice = voiceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
using Content.Shared.Humanoid.Markings;
|
using Content.Shared.Humanoid.Markings;
|
||||||
using Content.Shared.Humanoid.Prototypes;
|
using Content.Shared.Humanoid.Prototypes;
|
||||||
|
using Content.Shared.White.TTS;
|
||||||
using Robust.Shared.Enums;
|
using Robust.Shared.Enums;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Shared.Humanoid;
|
namespace Content.Shared.Humanoid;
|
||||||
@@ -71,6 +73,9 @@ public sealed partial class HumanoidAppearanceComponent : Component
|
|||||||
[DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public Color EyeColor = Color.Brown;
|
public Color EyeColor = Color.Brown;
|
||||||
|
|
||||||
|
[DataField("voice", customTypeSerializer: typeof(PrototypeIdSerializer<TTSVoicePrototype>))]
|
||||||
|
public string Voice { get; set; } = SharedHumanoidAppearanceSystem.DefaultVoice;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hair color of this humanoid. Used to avoid looping through all markings
|
/// Hair color of this humanoid. Used to avoid looping through all markings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
|||||||
|
|
||||||
[ValidatePrototypeId<SpeciesPrototype>]
|
[ValidatePrototypeId<SpeciesPrototype>]
|
||||||
public const string DefaultSpecies = "Human";
|
public const string DefaultSpecies = "Human";
|
||||||
|
public const string DefaultVoice = "Eugene";
|
||||||
|
public static readonly Dictionary<Sex, string> DefaultSexVoice = new()
|
||||||
|
{
|
||||||
|
{Sex.Male, "Eugene"},
|
||||||
|
{Sex.Female, "Kseniya"},
|
||||||
|
{Sex.Unsexed, "Xenia"},
|
||||||
|
};
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Content.Shared.Humanoid.Prototypes;
|
|||||||
using Content.Shared.Random.Helpers;
|
using Content.Shared.Random.Helpers;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Content.Shared.Traits;
|
using Content.Shared.Traits;
|
||||||
|
using Content.Shared.White.TTS;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Enums;
|
using Robust.Shared.Enums;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
@@ -37,6 +38,7 @@ namespace Content.Shared.Preferences
|
|||||||
string species,
|
string species,
|
||||||
int age,
|
int age,
|
||||||
Sex sex,
|
Sex sex,
|
||||||
|
string voice,
|
||||||
Gender gender,
|
Gender gender,
|
||||||
HumanoidCharacterAppearance appearance,
|
HumanoidCharacterAppearance appearance,
|
||||||
ClothingPreference clothing,
|
ClothingPreference clothing,
|
||||||
@@ -49,6 +51,7 @@ namespace Content.Shared.Preferences
|
|||||||
Name = name;
|
Name = name;
|
||||||
FlavorText = flavortext;
|
FlavorText = flavortext;
|
||||||
Species = species;
|
Species = species;
|
||||||
|
Voice = voice;
|
||||||
Age = age;
|
Age = age;
|
||||||
Sex = sex;
|
Sex = sex;
|
||||||
Gender = gender;
|
Gender = gender;
|
||||||
@@ -67,7 +70,7 @@ namespace Content.Shared.Preferences
|
|||||||
Dictionary<string, JobPriority> jobPriorities,
|
Dictionary<string, JobPriority> jobPriorities,
|
||||||
List<string> antagPreferences,
|
List<string> antagPreferences,
|
||||||
List<string> traitPreferences)
|
List<string> traitPreferences)
|
||||||
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack,
|
: this(other.Name, other.FlavorText, other.Species, other.Voice, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack,
|
||||||
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences)
|
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -82,6 +85,7 @@ namespace Content.Shared.Preferences
|
|||||||
string name,
|
string name,
|
||||||
string flavortext,
|
string flavortext,
|
||||||
string species,
|
string species,
|
||||||
|
string voice,
|
||||||
int age,
|
int age,
|
||||||
Sex sex,
|
Sex sex,
|
||||||
Gender gender,
|
Gender gender,
|
||||||
@@ -92,7 +96,7 @@ namespace Content.Shared.Preferences
|
|||||||
PreferenceUnavailableMode preferenceUnavailable,
|
PreferenceUnavailableMode preferenceUnavailable,
|
||||||
IReadOnlyList<string> antagPreferences,
|
IReadOnlyList<string> antagPreferences,
|
||||||
IReadOnlyList<string> traitPreferences)
|
IReadOnlyList<string> traitPreferences)
|
||||||
: this(name, flavortext, species, age, sex, gender, appearance, clothing, backpack, new Dictionary<string, JobPriority>(jobPriorities),
|
: this(name, flavortext, species, age, sex, voice, gender, appearance, clothing, backpack, new Dictionary<string, JobPriority>(jobPriorities),
|
||||||
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences))
|
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -106,6 +110,7 @@ namespace Content.Shared.Preferences
|
|||||||
"John Doe",
|
"John Doe",
|
||||||
"",
|
"",
|
||||||
SharedHumanoidAppearanceSystem.DefaultSpecies,
|
SharedHumanoidAppearanceSystem.DefaultSpecies,
|
||||||
|
SharedHumanoidAppearanceSystem.DefaultVoice,
|
||||||
18,
|
18,
|
||||||
Sex.Male,
|
Sex.Male,
|
||||||
Gender.Male,
|
Gender.Male,
|
||||||
@@ -133,6 +138,7 @@ namespace Content.Shared.Preferences
|
|||||||
"John Doe",
|
"John Doe",
|
||||||
"",
|
"",
|
||||||
species,
|
species,
|
||||||
|
SharedHumanoidAppearanceSystem.DefaultVoice,
|
||||||
18,
|
18,
|
||||||
Sex.Male,
|
Sex.Male,
|
||||||
Gender.Male,
|
Gender.Male,
|
||||||
@@ -176,11 +182,16 @@ namespace Content.Shared.Preferences
|
|||||||
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
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var voiceId = random.Pick(prototypeManager
|
||||||
|
.EnumeratePrototypes<TTSVoicePrototype>()
|
||||||
|
.Where(o => CanHaveVoice(o, sex)).ToArray()
|
||||||
|
).ID;
|
||||||
|
|
||||||
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
|
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
|
||||||
|
|
||||||
var name = GetName(species, gender);
|
var name = GetName(species, gender);
|
||||||
|
|
||||||
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack,
|
return new HumanoidCharacterProfile(name, "", species, voiceId, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack,
|
||||||
new Dictionary<string, JobPriority>
|
new Dictionary<string, JobPriority>
|
||||||
{
|
{
|
||||||
{SharedGameTicker.FallbackOverflowJob, JobPriority.High},
|
{SharedGameTicker.FallbackOverflowJob, JobPriority.High},
|
||||||
@@ -191,6 +202,8 @@ namespace Content.Shared.Preferences
|
|||||||
public string FlavorText { get; private set; }
|
public string FlavorText { get; private set; }
|
||||||
public string Species { get; private set; }
|
public string Species { get; private set; }
|
||||||
|
|
||||||
|
public string Voice { get; private set; }
|
||||||
|
|
||||||
[DataField("age")]
|
[DataField("age")]
|
||||||
public int Age { get; private set; }
|
public int Age { get; private set; }
|
||||||
|
|
||||||
@@ -211,6 +224,12 @@ namespace Content.Shared.Preferences
|
|||||||
public IReadOnlyList<string> TraitPreferences => _traitPreferences;
|
public IReadOnlyList<string> TraitPreferences => _traitPreferences;
|
||||||
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; }
|
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
public HumanoidCharacterProfile WithVoice(string voice)
|
||||||
|
{
|
||||||
|
return new(this) { Voice = voice };
|
||||||
|
}
|
||||||
|
|
||||||
public HumanoidCharacterProfile WithName(string name)
|
public HumanoidCharacterProfile WithName(string name)
|
||||||
{
|
{
|
||||||
return new(this) { Name = name };
|
return new(this) { Name = name };
|
||||||
@@ -500,6 +519,15 @@ namespace Content.Shared.Preferences
|
|||||||
|
|
||||||
_traitPreferences.Clear();
|
_traitPreferences.Clear();
|
||||||
_traitPreferences.AddRange(traits);
|
_traitPreferences.AddRange(traits);
|
||||||
|
|
||||||
|
prototypeManager.TryIndex<TTSVoicePrototype>(Voice, out var voice);
|
||||||
|
if (voice is null || !CanHaveVoice(voice, Sex))
|
||||||
|
Voice = SharedHumanoidAppearanceSystem.DefaultSexVoice[sex];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanHaveVoice(TTSVoicePrototype voice, Sex sex)
|
||||||
|
{
|
||||||
|
return voice.RoundStart && sex == Sex.Unsexed || (voice.Sex == sex || voice.Sex == Sex.Unsexed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// sorry this is kind of weird and duplicated,
|
// sorry this is kind of weird and duplicated,
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ public enum VoiceMaskUIKey : byte
|
|||||||
public sealed class VoiceMaskBuiState : BoundUserInterfaceState
|
public sealed class VoiceMaskBuiState : BoundUserInterfaceState
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
public string Voice { get; }
|
||||||
|
|
||||||
public VoiceMaskBuiState(string name)
|
public VoiceMaskBuiState(string name, string voice)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
|
Voice = voice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
Content.Shared/White/TTS/PlayTTSEvent.cs
Normal file
17
Content.Shared/White/TTS/PlayTTSEvent.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.White.TTS;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed class PlayTTSEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public NetEntity Uid { get; }
|
||||||
|
public byte[] Data { get; }
|
||||||
|
|
||||||
|
public PlayTTSEvent(NetEntity uid, byte[] data)
|
||||||
|
{
|
||||||
|
Uid = uid;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Content.Shared/White/TTS/RequestTTSEvent.cs
Normal file
15
Content.Shared/White/TTS/RequestTTSEvent.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.White.TTS;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed class RequestTTSEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
public RequestTTSEvent(string text)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Content.Shared/White/TTS/SharedVoiceMaskSystem.cs
Normal file
14
Content.Shared/White/TTS/SharedVoiceMaskSystem.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.VoiceMask;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class VoiceMaskChangeVoiceMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public string Voice { get; }
|
||||||
|
|
||||||
|
public VoiceMaskChangeVoiceMessage(string voice)
|
||||||
|
{
|
||||||
|
Voice = voice;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Content.Shared/White/TTS/TTSVoicePrototype.cs
Normal file
34
Content.Shared/White/TTS/TTSVoicePrototype.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Content.Shared.Humanoid;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Shared.White.TTS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prototype represent available TTS voices
|
||||||
|
/// </summary>
|
||||||
|
[Prototype("ttsVoice")]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public sealed class TTSVoicePrototype : IPrototype
|
||||||
|
{
|
||||||
|
[IdDataFieldAttribute]
|
||||||
|
public string ID { get; } = default!;
|
||||||
|
|
||||||
|
[DataField("name")]
|
||||||
|
public string Name { get; } = string.Empty;
|
||||||
|
|
||||||
|
[DataField("sex", required: true)]
|
||||||
|
public Sex Sex { get; } = default!;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("speaker", required: true)]
|
||||||
|
public string Speaker { get; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the species is available "at round start" (In the character editor)
|
||||||
|
/// </summary>
|
||||||
|
[DataField("roundStart")]
|
||||||
|
public bool RoundStart { get; } = true;
|
||||||
|
|
||||||
|
[DataField("sponsorOnly")]
|
||||||
|
public bool SponsorOnly { get; } = false;
|
||||||
|
}
|
||||||
@@ -62,4 +62,32 @@ public sealed class WhiteCVars
|
|||||||
|
|
||||||
public static readonly CVarDef<string> UtkaSocketKey = CVarDef.Create("utka.socket_key", "ass", CVar.SERVERONLY | CVar.CONFIDENTIAL);
|
public static readonly CVarDef<string> UtkaSocketKey = CVarDef.Create("utka.socket_key", "ass", CVar.SERVERONLY | CVar.CONFIDENTIAL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTS (Text-To-Speech)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the TTS server API.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<bool> TTSEnabled =
|
||||||
|
CVarDef.Create("tts.enabled", true, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the TTS server API.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<string> TTSApiUrl =
|
||||||
|
CVarDef.Create("tts.api_url", "http://127.0.0.1:2386", CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TTS Volume
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<float> TtsVolume =
|
||||||
|
CVarDef.Create("tts.volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TTS Cache
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<int> TTSMaxCacheSize =
|
||||||
|
CVarDef.Create("tts.max_cash_size", 200, CVar.SERVERONLY | CVar.ARCHIVE);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,12 @@
|
|||||||
- key: enum.StrippingUiKey.Key
|
- key: enum.StrippingUiKey.Key
|
||||||
type: StrippableBoundUserInterface
|
type: StrippableBoundUserInterface
|
||||||
- type: Puller
|
- type: Puller
|
||||||
|
- type: Butcherable
|
||||||
|
butcheringType: Spike # TODO human.
|
||||||
|
spawned:
|
||||||
|
- id: FoodMeat
|
||||||
|
amount: 5
|
||||||
|
- type: TTS
|
||||||
- type: Speech
|
- type: Speech
|
||||||
speechSounds: Alto
|
speechSounds: Alto
|
||||||
- type: DamageForceSay
|
- type: DamageForceSay
|
||||||
|
|||||||
29
Resources/Prototypes/White/tts-voices.yml
Normal file
29
Resources/Prototypes/White/tts-voices.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
- type: ttsVoice
|
||||||
|
id: Aidar
|
||||||
|
name: tts-voice-name-aidar
|
||||||
|
sex: Male
|
||||||
|
speaker: aidar
|
||||||
|
|
||||||
|
- type: ttsVoice
|
||||||
|
id: Eugene
|
||||||
|
name: tts-voice-name-eugene
|
||||||
|
sex: Male
|
||||||
|
speaker: eugene
|
||||||
|
|
||||||
|
- type: ttsVoice
|
||||||
|
id: Baya
|
||||||
|
name: tts-voice-name-baya
|
||||||
|
sex: Female
|
||||||
|
speaker: baya
|
||||||
|
|
||||||
|
- type: ttsVoice
|
||||||
|
id: Kseniya
|
||||||
|
name: tts-voice-name-kseniya
|
||||||
|
sex: Female
|
||||||
|
speaker: kseniya
|
||||||
|
|
||||||
|
- type: ttsVoice
|
||||||
|
id: Xenia
|
||||||
|
name: tts-voice-name-xenia
|
||||||
|
sex: Female
|
||||||
|
speaker: xenia
|
||||||
Reference in New Issue
Block a user